<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Giuliano Losa - Blog</title>
  <subtitle>Research homepage of Giuliano Losa, a researcher in distributed computing and formal methods.</subtitle>
  <link rel="self" type="application/atom+xml" href="https://www.losa.fr/feed.xml"/>
  <link rel="alternate" type="text/html" href="https://www.losa.fr/blog/"/>
  <generator uri="https://www.getzola.org/">Zola</generator>
  <updated>2026-06-20T00:00:00+00:00</updated>
  <id>https://www.losa.fr/feed.xml</id>
  
    
      <entry xml:lang="en">
        <title>Signature-Free BFT Consensus in Three Steps</title>
        <published>2026-06-16T00:00:00+00:00</published>
        <updated>2026-06-20T00:00:00+00:00</updated>
        
          <author>
            <name>Giuliano Losa</name>
          </author>
        
        <link rel="alternate" type="text/html" href="https://www.losa.fr/blog/how-forget-it-commits-in-three-steps/"/>
        <id>https://www.losa.fr/blog/how-forget-it-commits-in-three-steps/</id>
        
          <content type="html" xml:base="https://www.losa.fr/blog/how-forget-it-commits-in-three-steps/">&lt;p&gt;&lt;strong&gt;Update, June 16, 2026:&lt;&#x2F;strong&gt; Revised the framing after first publication.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Update, June 20, 2026:&lt;&#x2F;strong&gt; Simplified the protocol (merged witnesses and cores).&lt;&#x2F;p&gt;
&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;&#x2F;h1&gt;
&lt;p&gt;Recently, Abraham et al. presented &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;eprint.iacr.org&#x2F;2026&#x2F;355&quot;&gt;Forget-IT&lt;&#x2F;a&gt;, a new partially-synchronous, signature-free BFT consensus protocol for $n=3f+1$ parties among which $f$ may be Byzantine.
Forget-IT achieves two properties never achieved in combination before&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-it-protocols-1&quot;&gt;&lt;a href=&quot;#fn-it-protocols&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;: (a) when the network is synchronous, it commits a correct leader’s proposal in just three message delays, and (b) even in the worst case, it sends only $O(n^2)$ bits per view.
Having tried and failed to obtain such a protocol before, I set out to understand the core idea behind it.&lt;&#x2F;p&gt;
&lt;p&gt;I initially framed this blog post as distilling Forget-IT down to the core mechanism that allows it to achieve points (a) and (b).
However, after first publishing this blog post, I realized that I must have had elements of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;decentralizedthoughts.github.io&#x2F;2026-06-05-IT-Kuplex&#x2F;&quot;&gt;IT-Kuplex&lt;&#x2F;a&gt; in mind too.
So, let us say that we are going to sketch how to obtain properties (a) and (b) using simple quorum patterns inspired by both works.&lt;&#x2F;p&gt;
&lt;p&gt;As Eli Gafni would say, the key to solving consensus is to first solve the adopt-commit problem&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-gafni-rrfd-1&quot;&gt;&lt;a href=&quot;#fn-gafni-rrfd&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
In fact, solving our consensus problem reduces to solving adopt-commit with a good-case latency of two message delays while sending $O(n^2)$ bits in the worst case.
Essentially, each consensus view can be implemented using two consecutive instances of adopt-commit: the first to try to commit the leader’s proposal, the second to lock the committed value, if any, before the next view (we leave the details as an exercise to the reader).
So we will now focus on solving adopt-commit, and this will expose clearly some interesting quorum patters found in Forget-IT and IT-Kuplex.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;adopt-commit&quot;&gt;Adopt-Commit&lt;&#x2F;h1&gt;
&lt;p&gt;Adopt-commit is a single-shot abstraction that, unlike consensus, is solvable asynchronously even with failures.&lt;&#x2F;p&gt;
&lt;p&gt;In the adopt-commit problem, each party receives an input value and must eventually &lt;em&gt;commit&lt;&#x2F;em&gt; or &lt;em&gt;adopt&lt;&#x2F;em&gt; a unique value, such that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Validity: if a correct party commits or adopts $v$, then $v$ is the input of a correct party.&lt;&#x2F;li&gt;
&lt;li&gt;Agreement: if a correct party commits $v$, then no correct party commits or adopts a different value.&lt;&#x2F;li&gt;
&lt;li&gt;Unanimity: if all correct parties have the same input value, then they all commit that value.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here we consider a slightly relaxed variant of adopt-commit, which we call adopt-commit*, where parties do not terminate after adopting or committing a value and where we allow a party to adopt and then commit the same value.
This simplifies the solution and works fine if the goal is to use it to implement consensus.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;adopt-commit-in-two-message-delays-and-o-n-2-communication&quot;&gt;Adopt-Commit* in two message delays and $O(n^2)$ communication&lt;&#x2F;h1&gt;
&lt;p&gt;Now let us consider a message-passing system consisting of $n$ parties among which at most $f=\lfloor\frac{n-1}{3}\rfloor$ may be Byzantine while the others are “correct” (meaning they follow the protocol and keep taking steps).
We assume that the system is asynchronous: parties have no local clocks and message delay is unbounded.
However, the network is reliable: every message sent from a correct party to a correct party is eventually received.&lt;&#x2F;p&gt;
&lt;p&gt;Our task is to solve the adopt-commit* problem such that:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;If all correct parties receive the same input (the &lt;em&gt;good case&lt;&#x2F;em&gt;), then they all commit in two message delays.&lt;&#x2F;li&gt;
&lt;li&gt;In the worst case, correct parties together send at most $O(n^2)$ bits in total.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Here is an algorithm. It evolves in two phases.
First, each party broadcasts a vote for its input.
Second, parties exchange three types of message to let each other know what votes they received:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Commit messages.&lt;&#x2F;em&gt; A party sends a commit message for a value $v$ when it has received votes for $v$ from $n-f$ parties and it has not previously sent a no-core message or a commit or candidate message for a different value.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Candidate messages.&lt;&#x2F;em&gt; A party sends a candidate message for a value $v$ when it has received votes for $v$ from $f+1$ parties (excluding parties that sent multiple votes) and it has not previously sent a commit message for a different value.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;No-core messages.&lt;&#x2F;em&gt; A party sends a no-core message when it has received votes from $n-f$ parties (excluding parties that sent multiple votes) and, among those votes, no value got votes from a core of $f+1$ parties, and the party has not previously sent a commit message.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Finally, parties output according to the following rules:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If a party receives commit messages for $v$ from $n-f$ parties, it commits $v$.&lt;&#x2F;li&gt;
&lt;li&gt;If a party has not output yet and it receives commit or candidate messages for $v$ from $n-f$ parties, it adopts $v$.&lt;&#x2F;li&gt;
&lt;li&gt;If a party has not output yet and it receives no-core messages from $n-f$ parties, it adopts its own input.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;correctness-proof&quot;&gt;Correctness proof&lt;&#x2F;h1&gt;
&lt;p&gt;We must show the following:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The algorithm satisfies the validity and agreement properties.&lt;&#x2F;li&gt;
&lt;li&gt;Every correct party eventually adopts or commits a value.&lt;&#x2F;li&gt;
&lt;li&gt;If all correct parties have the same input, then they all commit in two message delays.&lt;&#x2F;li&gt;
&lt;li&gt;Correct parties together send $O(n^2)$ bits in the worst case.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;First, let us define some vocabulary.
Call sets of at least $n-f$ parties &lt;em&gt;quorums&lt;&#x2F;em&gt; and sets of at least $f+1$ parties &lt;em&gt;cores&lt;&#x2F;em&gt;.
Say a set is correct when all its members are correct.
The algorithm depends on the following properties of quorums and cores.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Quorum intersection:&lt;&#x2F;em&gt; Every two quorums have a correct party in common (because $2(n-f)-n&amp;gt;f$).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Quorum availability:&lt;&#x2F;em&gt; Correct parties form a quorum.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Quorum validity:&lt;&#x2F;em&gt; Every quorum contains a core of correct parties (because $(n-f)-f=n-2f\geq f+1$).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Core validity:&lt;&#x2F;em&gt; Every core contains a correct party (because a core has $f+1$ parties and at most $f$ are Byzantine).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Core&#x2F;quorum intersection:&lt;&#x2F;em&gt; Every correct core set and every quorum have a correct party in common (because $(f+1)+(n-f)-n&amp;gt;0$).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;validity-and-agreement&quot;&gt;Validity and Agreement&lt;&#x2F;h2&gt;
&lt;p&gt;Validity is trivial.
Agreement is not too hard:
Note that no correct party broadcasts both a commit message for a value $v$ and either a commit&#x2F;candidate message for a different value or a no-core message (this follows purely from the local rules a party follows).
Thus, by quorum intersection, it is impossible for a correct party to commit a value $v$ and another correct party to commit or adopt a different value.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unanimity-and-good-case-latency&quot;&gt;Unanimity and good-case latency&lt;&#x2F;h2&gt;
&lt;p&gt;Next, let us show that, if all correct parties have the same input, then they all commit in two message delays.
This covers both the unanimity property and the good-case latency.
Suppose all correct parties have the same input $v$.
It follows that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;By core validity, no correct party ever broadcasts a candidate message for a value other than $v$.&lt;&#x2F;li&gt;
&lt;li&gt;By quorum validity, no correct party ever broadcasts a commit message for a value other than $v$.&lt;&#x2F;li&gt;
&lt;li&gt;By quorum validity, no correct party ever broadcasts a no-core message.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Thus, by quorum availability, every correct party will eventually broadcast a commit message for $v$ and will eventually commit $v$ after two message delays.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liveness&quot;&gt;Liveness&lt;&#x2F;h2&gt;
&lt;p&gt;It remains to show that every correct party eventually commits or adopts a value.
Consider the following two exhaustive cases.&lt;&#x2F;p&gt;
&lt;p&gt;First, assume there is a value $v$ such that at least $f+1$ correct parties (a core set) have input $v$.
Then, by core&#x2F;quorum intersection, no correct party broadcasts a commit message for a value other than $v$.
Moreover, every correct party eventually receives those core votes and broadcasts a candidate message for $v$; by quorum availability, every correct party adopts $v$ unless it has already adopted a value.&lt;&#x2F;p&gt;
&lt;p&gt;Second, assume there is no value $v$ such that at least $f+1$ correct parties have input $v$.
Then, by quorum validity, no correct party ever broadcasts a commit message.
Moreover, every correct party eventually receives the votes from all correct parties, which form a quorum in which no core voted for the same value.
Hence, every correct party broadcasts a no-core message and then adopts its own input, unless it has already adopted a value.&lt;&#x2F;p&gt;
&lt;p&gt;In both cases, we concluded that every correct party adopts a value, and so we are done.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;communication-complexity&quot;&gt;Communication complexity&lt;&#x2F;h2&gt;
&lt;p&gt;Finally, we must show that correct parties only send $O(n^2)$ bits in total.
Assuming values are of constant size, it suffices to observe that each party broadcasts at most six messages: one vote message, one no-core message, one commit message, and three candidate messages (each distinct candidate needs a disjoint core of $f+1$ non-equivocating voters; since $n\leq 3(f+1)$, there can be at most three).&lt;&#x2F;p&gt;
&lt;p&gt;Note that, if we used $f&amp;lt;\lfloor\frac{n-1}{3}\rfloor$, we would increase the number of possible candidate messages.
In the general case of $f&amp;lt;\frac{n}{3}$ we may get a linear number of candidate messages and an overall bit complexity of $O(n^3)$.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mechanically-checked-proofs-in-ivy&quot;&gt;Mechanically-checked proofs in Ivy&lt;&#x2F;h2&gt;
&lt;p&gt;A formalization in Ivy is included below, and mechanically-checked proofs of safety and liveness are available at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;2-step-sf-bft-adopt-commit&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;2-step-sf-bft-adopt-commit&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;epilogue&quot;&gt;Epilogue&lt;&#x2F;h1&gt;
&lt;p&gt;So what is the key insight?
I think it is the 3-way case split and the three corresponding message types:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If all correct parties have the same input $v$, then every correct party gets a quorum of “commit” messages for $v$ in two message delays.&lt;&#x2F;li&gt;
&lt;li&gt;If some value $v$ has a correct core ($f+1$ correct processes with the same input), then that core prevents commits for conflicting values, and we eventually get a quorum of candidate messages for $v$.&lt;&#x2F;li&gt;
&lt;li&gt;If no correct core has the same input, then we eventually get a quorum of “no-core” messages.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;On top of that, local exclusion rules prevent a party from sending both a commit message and either a commit&#x2F;candidate message for a different value or a no-core message; thus, by quorum intersection, we have agreement.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;appendix-ivy-protocol-model&quot;&gt;Appendix: Ivy protocol model&lt;&#x2F;h1&gt;
&lt;p&gt;For reference, here is the core Ivy model of the protocol.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;ivy&quot;&gt;#lang ivy1.7

################################################################################
# Types
################################################################################

type party
type val

# The set types below abstract the threshold certificates from the writeup.
# Cardinalities are not encoded directly; the axioms capture the threshold facts
# used by the protocol for n &amp;gt;= 3f+1 and at most f Byzantine parties.

# A quorum represents at least n-f distinct senders.
type quorum

# A core represents at least f+1 distinct senders.
type core

################################################################################
# Immutable state and quorum theory
################################################################################

relation faulty(P:party)

relation quorum_member(Q:quorum, P:party)
relation core_member(S:core, P:party)

# Quorum intersection
axiom [quorum_intersection]
    exists P. ~faulty(P) &amp;amp; quorum_member(Q1, P) &amp;amp; quorum_member(Q2, P)

# Quorum availability
axiom [non_faulty_quorum]
    exists Q. forall P. quorum_member(Q, P) -&amp;gt; ~faulty(P)

# Every quorum contains a core of nonfaulty members.
axiom [quorum_contains_non_faulty_core]
    exists S. forall P.
        core_member(S, P) -&amp;gt; ~faulty(P) &amp;amp; quorum_member(Q, P)

# Every core has a nonfaulty member.
axiom [core_has_non_faulty_member]
    exists P. ~faulty(P) &amp;amp; core_member(S, P)

# A nonfaulty core intersects a quorum in a nonfaulty party.
axiom [core_quorum_intersection]
    (forall P. core_member(S, P) -&amp;gt; ~faulty(P))
    -&amp;gt;
    exists P. ~faulty(P) &amp;amp; core_member(S, P) &amp;amp; quorum_member(Q, P)

################################################################################
# Mutable protocol state
################################################################################

# First-round value votes.
relation vote(P:party, V:val)

# Second-round support.
relation candidate(P:party, V:val)
relation commit(P:party, V:val)
relation no_core(P:party)

# Output state. Parties may both commit and adopt (the same value, obviously),
# and keep sending messages after output.
relation commit_out(P:party, V:val)
relation adopt_support_out(P:party, V:val)
relation adopt_no_core_out(P:party, V:val)

################################################################################
# Derived predicates
################################################################################

relation adopt_out(P:party, V:val)
definition adopt_out(P, V) = adopt_support_out(P, V) | adopt_no_core_out(P, V)

relation output(P:party, V:val)
definition output(P, V) = commit_out(P, V) | adopt_out(P, V)

relation started(P:party)
definition started(P) = exists V. vote(P, V)

relation correct_input(V:val)
definition correct_input(V) = exists P. ~faulty(P) &amp;amp; vote(P, V)

################################################################################
# Initialization
################################################################################

after init {
    vote(P, V) := false;
    candidate(P, V) := false;
    commit(P, V) := false;
    no_core(P) := false;
    commit_out(P, V) := false;
    adopt_support_out(P, V) := false;
    adopt_no_core_out(P, V) := false;
}

################################################################################
# Protocol actions
################################################################################

action start_step(p:party, v:val) = {
    require ~vote(p, V);
    vote(p, v) := true;
}

action commit_step(p:party, v:val, q:quorum) = {
    require quorum_member(q, P) -&amp;gt; vote(P, v);
    require ~commit(p, V);
    require ~no_core(p); # TODO: necessary?
    require candidate(p, V) -&amp;gt; V = v;
    commit(p, v) := true;
}

action candidate_step(p:party, v:val, w:core) = {
    require core_member(w, P) -&amp;gt; vote(P, v);
    require commit(p, V2) -&amp;gt; V2 = v;
    candidate(p, v) := true;
}

action no_core_step(p:party, q:quorum) = {
    require ~commit(p, V);
    require quorum_member(q, P) -&amp;gt; started(P);
    require
        (forall P . core_member(B, P) -&amp;gt; quorum_member(q, P))
        -&amp;gt; (exists P . core_member(B, P) &amp;amp; ~vote(P,V));
    no_core(p) := true;
}

action output_commit_step(p:party, v:val, q:quorum) = {
    require quorum_member(q, P) -&amp;gt; commit(P, v);
    commit_out(p, v) := true;
}

action output_adopt_step(p:party, v:val, q:quorum) = {
    require ~(adopt_out(p, V) | commit_out(p, V));
    require quorum_member(q, P) -&amp;gt; candidate(P, v) | commit(P, v);
    adopt_support_out(p, v) := true;
}

action output_no_core_step(p:party, v:val, q:quorum) = {
    require ~(adopt_out(p, V) | commit_out(p, V));
    require vote(p, v);
    require quorum_member(q, P) -&amp;gt; no_core(P);
    adopt_no_core_out(p, v) := true;
}

# Byzantine parties may equivocate and may change their sent-message relations
# arbitrarily.
action byz_party(p:party) = {
    require faulty(p);
    vote(p, V) := *;
    candidate(p, V) := *;
    commit(p, V) := *;
    no_core(p) := *;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-it-protocols&quot;&gt;
&lt;p&gt;A solution that commits in three message delays in the good case but sends $O(n^3)$ bits per view in the worst case appears in Chapter 3 of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pmg.csail.mit.edu&#x2F;~castro&#x2F;thesis.pdf&quot;&gt;the PhD thesis of Miguel Castro&lt;&#x2F;a&gt;. Solutions with $O(n^2)$ bits per view but more than three message delays in the good case include &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;10.1145&#x2F;3662158.3662783&quot;&gt;TetraBFT&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;drops.dagstuhl.de&#x2F;entities&#x2F;document&#x2F;10.4230&#x2F;LIPIcs.OPODIS.2020.11&quot;&gt;IT-HS&lt;&#x2F;a&gt;. &lt;a href=&quot;#fr-it-protocols-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-gafni-rrfd&quot;&gt;
&lt;p&gt;Adopt-commit was first presented at PODC 1998 by Eli Gafni in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;abs&#x2F;10.1145&#x2F;277697.277724&quot;&gt;&lt;em&gt;Round-by-round fault detectors: unifying synchrony and asynchrony&lt;&#x2F;em&gt;&lt;&#x2F;a&gt;. &lt;a href=&quot;#fr-gafni-rrfd-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</content>
        
      </entry>
    
  
    
      <entry xml:lang="en">
        <title>Streamlet in TLA+</title>
        <published>2022-01-04T00:00:00+00:00</published>
        <updated>2022-01-04T00:00:00+00:00</updated>
        
          <author>
            <name>Giuliano Losa</name>
          </author>
        
        <link rel="alternate" type="text/html" href="https://www.losa.fr/blog/streamlet-in-tla+/"/>
        <id>https://www.losa.fr/blog/streamlet-in-tla+/</id>
        
          <content type="html" xml:base="https://www.losa.fr/blog/streamlet-in-tla+/">&lt;p&gt;In this blog post, we will see how to specify the Streamlet algorithm in PlusCal&#x2F;TLA+ with a focus on writing simple specifications that are amenable to model-checking of both safety and liveness properties with TLC.&lt;&#x2F;p&gt;
&lt;p&gt;You can find the source code at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;streamlet&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;streamlet&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;context-and-results&quot;&gt;Context and results&lt;&#x2F;h1&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;eprint.iacr.org&#x2F;2020&#x2F;088.pdf&quot;&gt;Streamlet blockchain-consensus algorithm&lt;&#x2F;a&gt; is arguably one of the simplest blockchain-consensus algorithm.
What makes Streamlet simple is that there are only two types of messages (leader proposals and votes) and processes repeat the same, simple propose-vote pattern ad infinitum.
In contrast, protocols like Paxos or PBFT alternate between two sub-protocols: one for the normal case, when things go well, and a view-change sub-protocol to recover from failures.&lt;&#x2F;p&gt;
&lt;p&gt;The proofs in the Streamlet paper use the operational reasoning style, where we consider an entire execution and try to reason about the possible ordering of events in order to show by case analysis that the algorithm is correct.
In my experience, this proof style is very error-prone, and it is not easy to make sure that no case was overlooked.
I would prefer a proof based on inductive invariants, but that’s a discussion that’s off-topic for this post.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, in this post, we will specify the Streamlet algorithm in PlusCal&#x2F;TLA+ and use the TLC model-checker to verify its safety and liveness properties in small but non-trivial configurations.
Moreover, the specification I present are also an example of:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;how to use non-determinism to obtain simple specifications&lt;&#x2F;li&gt;
&lt;li&gt;how to exploit the commutativity of actions to speed-up model-checking by sequentializing the specification.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I was able to exhaustively check the safety and liveness properties of (crash-stop) Streamlet in interesting configurations:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;with 3 processes, 2 block payloads, and 7 asynchronous epochs;&lt;&#x2F;li&gt;
&lt;li&gt;with 3 processes, 2 block payloads, and 5 asynchronous epochs followed by 4 synchronous epochs (i.e. “GST” happens at the start of epoch 6).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Those results give me very high confidence that streamlet satisfies its claimed properties.&lt;&#x2F;p&gt;
&lt;p&gt;We’ll also see that TLC verifies that, in all configurations checked, Streamlet guarantees that a new block gets finalized in 4 synchronous rounds.
This is better than the bound of 5 rounds proved in the Streamlet paper, and I believe that a bound of 4 holds in general.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-streamlet-algorithm&quot;&gt;The Streamlet algorithm&lt;&#x2F;h1&gt;
&lt;p&gt;The goal of the Streamlet algorithm is to enable a fixed set of &lt;code&gt;N&lt;&#x2F;code&gt; processes in a message-passing network to iteratively construct a unique and ever-growing blockchain.
Although many such algorithms existed before Streamlet, Streamlet is striking because of the simplicity of the rules that processes must follow.&lt;&#x2F;p&gt;
&lt;p&gt;Streamlet can tolerate malicious, Byzantine processes, but, to simplify things, here we consider only crash-stop faults and we assume that, in every execution, a strict majority of the processes do not fail.
As is customary, we refer to a strict majority as a quorum.&lt;&#x2F;p&gt;
&lt;p&gt;The protocol evolves in consecutive epochs numbered 1,2,3,… during which processes vote for blocks according to the rules below.&lt;&#x2F;p&gt;
&lt;p&gt;A block consists of a hash of a previous block, an epoch number, and a payload (e.g. a set of transactions); moreover, a special, unique genesis block has epoch number 0.
Thus a set of blocks forms a directed graph such that &lt;code&gt;(b1,b2)&lt;&#x2F;code&gt; is an edge if and only if &lt;code&gt;b2&lt;&#x2F;code&gt; contains the hash of block &lt;code&gt;b1&lt;&#x2F;code&gt;.
We say that a set of blocks forms a valid block tree when the directed graph formed by the blocks is a
tree rooted at the genesis block. A valid blockchain (or simply a chain for short) is a valid block tree in which every block has at most one successor, i.e. in which there are no forks.&lt;&#x2F;p&gt;
&lt;p&gt;Each epoch &lt;code&gt;e&lt;&#x2F;code&gt; has a unique, pre-determined leader (e.g. process &lt;code&gt;(e mod N)+1&lt;&#x2F;code&gt;), and processes in epoch e must follow the following rules:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The leader proposes a new block with epoch number &lt;code&gt;e&lt;&#x2F;code&gt; that extends one of the longest notarized chains that the leader knows of (where notarized is defined below).&lt;&#x2F;li&gt;
&lt;li&gt;Every process votes for the leader’s proposal as long as the proposal is longer than the longest notarized chains that the process ever voted to extend.&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-note1-1&quot;&gt;&lt;a href=&quot;#fn-note1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;li&gt;
&lt;li&gt;A block is notarized when it has gathered votes from a quorum in the same epoch, and a chain is notarized when all its blocks, except the genesis block, are notarized.&lt;&#x2F;li&gt;
&lt;li&gt;When a notarized chain includes three adjacent blocks with consecutive epoch numbers, the prefix of the chain up to the second of those 3 blocks is considered final.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Process proceed from one epoch to the next through unspecified means.
In practice, a process may increment its epoch using a real-time clock (e.g. each epoch lasting 2 seconds), or, even though I don’t think this is discussed in the Streamlet paper, processes may use a synchronizer sub-protocol.
The synchronizer approach is more robust than simply relying on clocks, and it is used by many deployed protocols.
Surprisingly, it dates back to the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;groups.csail.mit.edu&#x2F;tds&#x2F;papers&#x2F;Lynch&#x2F;jacm88.pdf&quot;&gt;pioneering work of Dwork, Lynch, and Stockmeyer&lt;&#x2F;a&gt; in the 1980s.
For a recent treatment, see &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;2008.04167&quot;&gt;Gotsman et al.&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example-scenario&quot;&gt;Example scenario&lt;&#x2F;h2&gt;
&lt;p&gt;The following drawing illustrates a possible blocktree built by the Streamlet algorithm.
The tree vertices represent blocks with their epoch number and payloads omitted (we can assume that payloads are all different).
A vertex with a circle around it represents a notarized block, while a vertex without a circle is a block that has been voted for by at least one process but is not notarized.
The longest finalized blockchain is represented with full arrows, while other arrows are dotted.&lt;&#x2F;p&gt;
&lt;p&gt;We can see that the block with epoch 2 is notarized, thus finalizing the block with epoch 1, but then the leader of epoch 3 did not notice that block 2 was notarized and instead extended the block with epoch 1 with a block with epoch 3. This cause a fork in the tree of notarized blocks, but not a fork in the finalized blockchain.
Blocks with epoch number 3 and 4 in the finalized chain are final because of the notarized block with epoch 5.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;blocktree.svg&quot; alt=&quot;Possible block tree produced by the Streamlet algorithm&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;safety-guarantee&quot;&gt;Safety guarantee&lt;&#x2F;h2&gt;
&lt;p&gt;The algorithm guarantees that if two chains are final, then one is a prefix of the other.
This is the consistency property of the algorithm.&lt;&#x2F;p&gt;
&lt;p&gt;Note that the consistency guarantee holds even if processes proceed through epochs at different speeds and may not be in the same epoch at the same time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liveness-guarantee&quot;&gt;Liveness guarantee&lt;&#x2F;h2&gt;
&lt;p&gt;In an asynchronous network, Streamlet cannot guarantee that a block will ever get finalized.
This is because the consensus problem is famously unsolvable in an asynchronous network.
Instead, to guarantee liveness properties, we must make additional assumptions.
To do so, first define a synchronous epoch as an epoch in which all non-faulty processes receive each other’s messages before the end of the epoch, and in which the leader is not faulty.&lt;&#x2F;p&gt;
&lt;p&gt;We can now state Streamlet’s liveness guarantee:
The Streamlet algorithm guarantees that, after 4 consecutive synchronous epochs, a new blocks gets finalized.&lt;&#x2F;p&gt;
&lt;p&gt;Note that, according to the usual definitions of liveness and safety used in the academic field of distributed computing, this is a safety property because it can be violated in a bounded execution.
But, as in the Streamlet paper, we’ll call it liveness anyway.&lt;&#x2F;p&gt;
&lt;p&gt;Note that the Streamlet paper proves that we need 5 synchronous epochs to guarantee finalizing one more block, but I believe this is overly conservative and that 4 epochs suffice.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;streamlet-in-pluscal-tla&quot;&gt;Streamlet in PlusCal&#x2F;TLA+&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;blocks&quot;&gt;Blocks&lt;&#x2F;h2&gt;
&lt;p&gt;Processes in the Streamlet algorithm can be seen as building a block tree out of which emerges a unique, finalized blockchain.
Thus, we need to model blocks, block trees, and blockchains.&lt;&#x2F;p&gt;
&lt;p&gt;Except for the genesis block, a block consists of the hash of its parent block, an epoch, and a payload.
Thus, assuming that there are no hash collisions, a block uniquely determines all its ancestors up to the genesis block, or, equivalently, a unique sequence of epoch-payload pairs.
We could model a block as a recursive data structure containing its parent.
However, to make things simpler, we model a block as a sequence of pairs, each containing an epoch and a payload.
No information is lost in the process: in both cases, a block determines a unique sequence of epoch-payload pairs.&lt;&#x2F;p&gt;
&lt;p&gt;For example, this is a block in TLA+ notation:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;&amp;lt;&amp;lt; &amp;lt;&amp;lt;1, tx1&amp;gt;&amp;gt;, &amp;lt;&amp;lt;3, tx3&amp;gt;&amp;gt;, &amp;lt;&amp;lt;4, tx4&amp;gt;&amp;gt; &amp;gt;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This TLA+ block models a real block consisting of epoch 4, payload &lt;code&gt;tx4&lt;&#x2F;code&gt;, and the hash of a previous block with epoch 3, payload &lt;code&gt;tx3&lt;&#x2F;code&gt;, and a hash of a previous block with epoch 1, payload &lt;code&gt;tx1&lt;&#x2F;code&gt;, and the hash of the genesis block.&lt;&#x2F;p&gt;
&lt;p&gt;In this model of blocks, a block tree is a prefix-closed set of blocks, and a blockchain is a block tree without branching.
Moreover, we can extend a block &lt;code&gt;b&lt;&#x2F;code&gt; just by appending an epoch-payload tuple to it.
Finally, the genesis block is the empty sequence &lt;code&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;We now define the epoch of a block &lt;code&gt;b&lt;&#x2F;code&gt; as &lt;code&gt;0&lt;&#x2F;code&gt; if &lt;code&gt;b&lt;&#x2F;code&gt; is the genesis block and otherwise as the epoch found in the last tuple in &lt;code&gt;b&lt;&#x2F;code&gt;.
In TLA+, this translates to:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;  Epoch(b) ==
      IF b = Genesis
      THEN 0
      ELSE b[Len(b)][1]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Moreover, the parent of a block &lt;code&gt;b&lt;&#x2F;code&gt; is the genesis block if &lt;code&gt;b&lt;&#x2F;code&gt; has length 1, and otherwise it’s the block obtained by removing the last element of &lt;code&gt;b&lt;&#x2F;code&gt;.
In TLA+:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;  Parent(b) ==
      IF Len(b) = 1
      THEN Genesis
      ELSE SubSeq(b, 1, Len(b)-1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;first-specification&quot;&gt;First specification&lt;&#x2F;h2&gt;
&lt;p&gt;We start with a specification that makes use of non-determinism in order to eschew irrelevant details and capture the essence of how Streamlet ensures safety.
The specification, appearing below and in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;streamlet&#x2F;blob&#x2F;main&#x2F;Streamlet.tla&quot;&gt;Streamlet.tla&lt;&#x2F;a&gt;, is very short.
It consists of a mere 44 numbered lines of PlusCal; the display below uses a few unnumbered continuation lines to keep the listing readable.&lt;&#x2F;p&gt;
&lt;figure class=&quot;code-listing&quot;&gt;
  &lt;figcaption&gt;Listing 1: A compact PlusCal model of Streamlet safety&lt;&#x2F;figcaption&gt;
  &lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;CONSTANTS
        P \* The set of processes
    ,   Tx \* Transaction sets (the payload in a block)
    ,   MaxEpoch
    ,   Quorum \* The set of quorums
    ,   Leader(_) \* Leader(e) is the leader of epoch e

1   --algorithm Streamlet {
2       variables
3           vote = [p \in P |-&amp;gt; {}], \* the votes cast by the processes
4           proposal = [e \in E |-&amp;gt; &amp;lt;&amp;lt;&amp;gt;&amp;gt;];
5       define {
6           E == 1..MaxEpoch \* the set of epochs
7           Genesis == &amp;lt;&amp;lt;&amp;gt;&amp;gt;
8           Epoch(b) == \* the epoch of a block
9               IF b = Genesis
10              THEN 0 \* the root is by convention a block with epoch 0
11              ELSE b[Len(b)][1]
12          Parent(b) == \* the parent of a block
                IF Len(b) = 1
                THEN Genesis
                ELSE SubSeq(b, 1, Len(b)-1)
13          Blocks == UNION {vote[p] : p \in P}
14          Notarized == {Genesis} \cup \* Genesis is considered notarized by default
15              { b \in Blocks :
                    \E Q \in Quorum :
                        \A p \in Q : b \in vote[p] }
16          Final(b) ==
17              &#x2F;\  \E tx \in Tx :
                    Append(b, &amp;lt;&amp;lt;Epoch(b)+1, tx&amp;gt;&amp;gt;) \in Notarized
18              &#x2F;\  Epoch(Parent(b)) = Epoch(b)-1
19          Safety ==
                \A b1,b2 \in {b \in Blocks : Final(b)} :
20                  Len(b1) &amp;lt;= Len(b2) =&amp;gt;
                        b1 = SubSeq(b2, 1, Len(b1))
21      }
22      process (proc \in P)
23          variables
24              epoch = 1, \* the current epoch of p
25              height = 0; \* height of the longest notarized chain that p voted to extend
26      {
27  l1:     while (epoch \in E) {
28              \* if leader, make a proposal:
29              if (Leader(epoch) = self) {
30                  with (
                        parent \in {
                            b \in Notarized :
                                height &amp;lt;= Len(b) &#x2F;\ Epoch(b) &amp;lt;= epoch
                        },
31                      tx \in Tx,
                        b = Append(parent, &amp;lt;&amp;lt;epoch, tx&amp;gt;&amp;gt;)
                    )
32                      proposal[epoch] := b
33              };
34              \* next, either vote for the leader&amp;#39;s proposal or skip:
35              either {
36                  when Len(proposal[epoch]) &amp;gt; height;
37                  vote[self] := @ \cup {proposal[epoch]};
38                  height := Len(proposal[epoch])-1
39              } or skip;
40              \* finally, go to the next epoch:
41              epoch := epoch + 1;
42          }
43      }
44  }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;&#x2F;figure&gt;
&lt;p&gt;Let me now describe the specification informally.&lt;&#x2F;p&gt;
&lt;p&gt;We have two global variables, &lt;code&gt;vote&lt;&#x2F;code&gt; and &lt;code&gt;proposal&lt;&#x2F;code&gt;, and two process-local variables, &lt;code&gt;epoch&lt;&#x2F;code&gt; and &lt;code&gt;height&lt;&#x2F;code&gt;, with the following meaning:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;For every process &lt;code&gt;p&lt;&#x2F;code&gt;, &lt;code&gt;vote[p]&lt;&#x2F;code&gt; is the set of all votes cast by &lt;code&gt;p&lt;&#x2F;code&gt; so far.&lt;&#x2F;li&gt;
&lt;li&gt;For every epoch &lt;code&gt;e&lt;&#x2F;code&gt;, &lt;code&gt;proposal[e]&lt;&#x2F;code&gt; is the leader’s proposal for epoch &lt;code&gt;e&lt;&#x2F;code&gt; unless &lt;code&gt;proposal[e]&lt;&#x2F;code&gt; is empty (i.e. equal to the empty sequence &lt;code&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;For every process, the local variable &lt;code&gt;epoch&lt;&#x2F;code&gt; is the current epoch the process.&lt;&#x2F;li&gt;
&lt;li&gt;For every process, the local variable &lt;code&gt;height&lt;&#x2F;code&gt; is the height of the longest notarized block that the process ever voted to extend.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Lines 6 to 20, we make a few useful definitions, most notably:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Line 14, a block is notarized when a quorum unanimously voted for it.&lt;&#x2F;li&gt;
&lt;li&gt;Line 16, a block &lt;code&gt;b&lt;&#x2F;code&gt; is final when:
&lt;ul&gt;
&lt;li&gt;line 17, &lt;code&gt;b&lt;&#x2F;code&gt; has a notarized child with epoch &lt;code&gt;Epoch(b)+1&lt;&#x2F;code&gt;, and&lt;&#x2F;li&gt;
&lt;li&gt;line 18, the epoch of &lt;code&gt;b&lt;&#x2F;code&gt;’s parent is &lt;code&gt;Epoch(b)-1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ul&gt;
&lt;li&gt;Finally, line 19, the algorithm is safe when every two final blocks are the same up to the length of the shortest.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now consider a process we will call &lt;code&gt;self&lt;&#x2F;code&gt; at line 27, when &lt;code&gt;self&lt;&#x2F;code&gt; is just starting it current epoch &lt;code&gt;epoch&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;First, lines 29 to 33, if &lt;code&gt;self&lt;&#x2F;code&gt; is the leader of the current epoch, &lt;code&gt;self&lt;&#x2F;code&gt; picks a notarized block show length is greater than &lt;code&gt;height&lt;&#x2F;code&gt;, extends it with a new payload &lt;code&gt;tx&lt;&#x2F;code&gt;, and proposes it for the epoch.
This is an example of how we use non-determinism to abstract over irrelevant details:
In the original Streamlet algorithm, a process creates a proposal by extending one of the longest notarized chains it knows of.
Here we abstract over this rule by allowing the process to pick an arbitrary notarized block.
This is a sound abstraction because it does not restrict the behaviors of the algorithm (in fact, it may add new behaviors).&lt;&#x2F;p&gt;
&lt;p&gt;Next, line 35, &lt;code&gt;self&lt;&#x2F;code&gt; either votes for the leader’s proposal or skips voting in this epoch&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Line 36, &lt;code&gt;self&lt;&#x2F;code&gt; checks that the proposal extends a block whose height is at least the height of last block that &lt;code&gt;self&lt;&#x2F;code&gt; voted to extend.
If so, &lt;code&gt;self&lt;&#x2F;code&gt; votes for the proposal (line 37) and updates &lt;code&gt;height&lt;&#x2F;code&gt; to reflect the fact that it just voted to extend a block of height equal to the length of the proposal minus one.&lt;&#x2F;li&gt;
&lt;li&gt;Alternatively, line 39, &lt;code&gt;self&lt;&#x2F;code&gt; skips voting in this epoch.
This models the case in which &lt;code&gt;self&lt;&#x2F;code&gt; did not receive the leader’s proposal, or the proposal is not at least of height equal to the height of the longest notarized block that &lt;code&gt;self&lt;&#x2F;code&gt; ever voted to extend.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Finally, line 41, &lt;code&gt;self&lt;&#x2F;code&gt; goes to the next epoch.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;model-checking-results&quot;&gt;Model-checking results&lt;&#x2F;h3&gt;
&lt;p&gt;With TLC, I was able to exhaustively model-check that the &lt;code&gt;Safety&lt;&#x2F;code&gt; property holds for 3 processes, 2 payloads, and 6 epochs.
This was done on a 24 core &lt;code&gt;Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz&lt;&#x2F;code&gt; with 40GB of memory allocated to TLC, and it took about 4 hours and 20 minutes.&lt;&#x2F;p&gt;
&lt;p&gt;I think this is an interesting configuration because we have multiple quorums (sets of 2 processes at least), branching even within a single epoch (because of the 2 payloads), and enough epochs to obtain finalized chains containing non-consecutive epoch numbers and having notarized but non-final branches.
Thus, the model-checking results give me high confidence that the Streamlet algorithm is indeed safe.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liveness-and-sequentialization&quot;&gt;Liveness and sequentialization&lt;&#x2F;h2&gt;
&lt;p&gt;To model-check the liveness property of Streamlet, we must modify our specification and introduce synchronous epochs.
Moreover, we’ll need to be able to exhaustively model-check the specification for more than 6 epochs to get meaningful results.
This is because the liveness property states that a new block must be decided after 5 synchronous epochs.
To truly test this claim, we need to check that it holds even after a few asynchronous epochs have had the chance to wreak as much havoc as possible.
Just taking 3 asynchronous epoch gives use 3+5=8 epochs to check.
Thus, we better find a way to speed up model-checking.&lt;&#x2F;p&gt;
&lt;p&gt;As we will next see, we can take advantage of the commutativity of some steps in the protocol to reduce the problem to checking only a restricted set of canonical executions.
This is very effective and will allow us to exhaustively check that, even after 5 totally asynchronous epochs, Streamlet guarantees that a new block is finalized after 5 synchronous epochs.
In fact, we’ll see that it only takes 4 synchronous epoch for a new block to be finalized.
I believe that this is true in general, and that the bound of 5 proved in the Streamlet paper is overly conservative.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sequentialization&quot;&gt;Sequentialization&lt;&#x2F;h3&gt;
&lt;p&gt;Consider an execution &lt;code&gt;e&lt;&#x2F;code&gt; of Streamlet and two steps &lt;code&gt;s1&lt;&#x2F;code&gt; and &lt;code&gt;s2&lt;&#x2F;code&gt; of two different processes &lt;code&gt;p1&lt;&#x2F;code&gt; and &lt;code&gt;p2&lt;&#x2F;code&gt; such that &lt;code&gt;s1&lt;&#x2F;code&gt; occurs right before &lt;code&gt;s2&lt;&#x2F;code&gt;, &lt;code&gt;s1&lt;&#x2F;code&gt; is a step of epoch &lt;code&gt;e1&lt;&#x2F;code&gt;, &lt;code&gt;s2&lt;&#x2F;code&gt; is a step of epoch &lt;code&gt;e2&lt;&#x2F;code&gt;, and &lt;code&gt;e2&amp;lt;e1&lt;&#x2F;code&gt;.
Note that the global state written by &lt;code&gt;s2&lt;&#x2F;code&gt; is never read by &lt;code&gt;s1&lt;&#x2F;code&gt; because a process in epoch &lt;code&gt;e1&lt;&#x2F;code&gt; only uses information from epoch smaller or equal to &lt;code&gt;e1&lt;&#x2F;code&gt;.
Moreover, if step &lt;code&gt;s2&lt;&#x2F;code&gt; can take place at some point in an execution, adding more steps of other processes epochs lower than &lt;code&gt;e2&lt;&#x2F;code&gt; before &lt;code&gt;s2&lt;&#x2F;code&gt; never prevents &lt;code&gt;s2&lt;&#x2F;code&gt; from taking place (i.e. the protocol is monotonic).&lt;&#x2F;p&gt;
&lt;p&gt;The previous paragraph shows that we can reorder all steps in an execution &lt;code&gt;e1&lt;&#x2F;code&gt; to obtain a new execution &lt;code&gt;e2&lt;&#x2F;code&gt; in which all steps of epoch &lt;code&gt;1&lt;&#x2F;code&gt; happen first, then all steps of epoch 2, then all steps of epoch 3, etc.
Crucially, the end state of the system in &lt;code&gt;e1&lt;&#x2F;code&gt; and &lt;code&gt;e2&lt;&#x2F;code&gt; are the same.
Thus, if we prove that all executions like &lt;code&gt;e2&lt;&#x2F;code&gt;, which we call sequentialized executions, are safe and live, then we can conclude that all executions are safe and live.
This is because we express safety and liveness as state predicates, and, by our crucial observation above, restricting ourselves to sequentialized executions does not change the set of reachable states.&lt;&#x2F;p&gt;
&lt;p&gt;Moreover, with a slightly more complex justification, we can also reorder the steps of different processes within the same epoch as long as the leader always takes the first step.
This means that we can schedule processes completely deterministically, as in a sequential program, without loosing any reachable states.&lt;&#x2F;p&gt;
&lt;p&gt;This is what we do in the specification &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;streamlet&#x2F;blob&#x2F;main&#x2F;SequentializedStreamlet.tla&quot;&gt;&lt;code&gt;SequentializedStreamlet.tla&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.
There, we specify a scheduler that schedules all processes deterministically.
The result is that the set of behaviors that the TLC model checker must explore is drastically reduced.
For example, it takes only about 15 minutes to exhaustively explore all executions with 3 processes, 2 payloads, and 6 epochs; in contrast, it took about 4 hours and 20 minutes with the previous specification.&lt;&#x2F;p&gt;
&lt;p&gt;Note that this style of reduction is well-known and was used by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;groups.csail.mit.edu&#x2F;tds&#x2F;papers&#x2F;Lynch&#x2F;jacm88.pdf&quot;&gt;Dwork, Lynch, and Stockmeyer&lt;&#x2F;a&gt; in 1984 in order to simplify reasoning about their algorithms.
Several recent frameworks use this type of reduction to help engineers design and verify their algorithms.
For example, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;dzufferey&#x2F;psync&quot;&gt;PSync&lt;&#x2F;a&gt; provides a programming language to develop consensus algorithms directly in a model somewhat similar to the sequential model we used, and an efficient runtime system to deploy such algorithms.
We have taken a rather ad-hoc and informally justified approach to our sequentialization of Streamlet. In contrast, methods such as &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bkragl.github.io&#x2F;papers&#x2F;pldi2020.pdf&quot;&gt;inductive sequentialization&lt;&#x2F;a&gt;, supported by the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;civl-verifier.github.io&#x2F;&quot;&gt;Civl verifier&lt;&#x2F;a&gt;, offer a principled approach to applying such reductions.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;expressing-the-liveness-property&quot;&gt;Expressing the liveness property&lt;&#x2F;h3&gt;
&lt;p&gt;To check the liveness property, we must first have a way to specify that epochs become synchronous after a given, fixed epoch.
To this end, we introduce a constant &lt;code&gt;GSE&lt;&#x2F;code&gt; (for global synchronization epoch) and we add constraints that model the fact that all epoch including and after &lt;code&gt;GSE&lt;&#x2F;code&gt; are synchronous.&lt;&#x2F;p&gt;
&lt;p&gt;Remember that in a synchronous epoch, every node receives the leader’s proposal and every node receives all the votes of the other nodes.
This is reflected as follows in the PlusCal&#x2F;TLA+ specification:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;In epoch &lt;code&gt;GSE&lt;&#x2F;code&gt; and after, nodes do not skip the epoch and vote for the leader’s proposal if the proposal is longer than the longest chain that the node has ever voted to extend.&lt;&#x2F;li&gt;
&lt;li&gt;In epoch &lt;code&gt;GSE&lt;&#x2F;code&gt;, the leader makes a proposal, but the proposal doesn’t necessarily extend a longest notarized chain because, even though the leader must receive all previous votes by the end of epoch &lt;code&gt;GSE&lt;&#x2F;code&gt;, it might not yet have by the time it makes its proposal.&lt;&#x2F;li&gt;
&lt;li&gt;In epoch &lt;code&gt;GSE+1&lt;&#x2F;code&gt; and above, the leader proposes to extend one of the longest notarized chains.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Given the above, we now state the liveness property as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;Liveness ==
    (epoch = GSE+4) =&amp;gt;
        \E b \in Blocks :
            Final(b) &#x2F;\ Epoch(b) &amp;gt;= GSE-1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In English, this states that by the beginning of epoch &lt;code&gt;GSE+4&lt;&#x2F;code&gt;, there is a final block whose epoch is greater or equal to &lt;code&gt;GSE-1&lt;&#x2F;code&gt;.
You might wonder where this constraint on the block’s epoch comes from.
The answer is that we want to show that a &lt;em&gt;new&lt;&#x2F;em&gt; block, i.e. a block which was not final in epoch &lt;code&gt;GSE&lt;&#x2F;code&gt;, is now final.
It is easy to see that, when &lt;code&gt;GSE&lt;&#x2F;code&gt; starts, no block with an epoch greater or equal to &lt;code&gt;e-1&lt;&#x2F;code&gt; can be final when epoch &lt;code&gt;e&lt;&#x2F;code&gt; starts.
Thus, any final block with an epoch greater or equal to &lt;code&gt;GSE-1&lt;&#x2F;code&gt; was not final when epoch &lt;code&gt;GSE&lt;&#x2F;code&gt; started and can be considered “new”.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-sequentialized-specification-with-liveness&quot;&gt;The sequentialized specification with liveness&lt;&#x2F;h3&gt;
&lt;p&gt;Omitting definitions that are the same as before, here is the sequentialized specification of Streamlet.
You can also find it in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nano-o&#x2F;streamlet&#x2F;blob&#x2F;main&#x2F;SequentializedStreamlet.tla&quot;&gt;SequentializedStreamlet.tla&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;figure class=&quot;code-listing&quot;&gt;
  &lt;figcaption&gt;Listing 2: Sequentialized Streamlet with the liveness check&lt;&#x2F;figcaption&gt;
  &lt;pre&gt;&lt;code data-lang=&quot;tla&quot;&gt;1   --algorithm Streamlet {
2       variables
3           height = [p \in P |-&amp;gt; 0], \* height of the longest notarized chain p voted to extend
4           votes = [p \in P |-&amp;gt; {}], \* the votes cast by the processes
5           epoch = 1, \* the current epoch
6           scheduled = {}, \* processes already scheduled in the current epoch
7           proposal = &amp;lt;&amp;lt;&amp;gt;&amp;gt;; \* the leader&amp;#39;s proposal for the current epoch
8       define {
9           NextProc ==
10              IF scheduled = {}
11              THEN CHOOSE p \in P : Leader(epoch) = p
12              ELSE CHOOSE p \in P : \neg p \in scheduled
13          \* It takes at most 4 epochs to finalize a new block:
14          Liveness ==
                (epoch = GSE+4) =&amp;gt;
                    \E b \in Blocks :
                        Final(b) &#x2F;\ Epoch(b) &amp;gt;= GSE-1
15      }
16      process (scheduler \in {&amp;quot;sched&amp;quot;})
17      {
18  l1:     while (epoch \in E) {
19              with (proc = NextProc) {
20                  \* if proc is leader, make a proposal:
21                  if (Leader(epoch) = proc)
22                      with (
                            parent \in {
                                b \in Notarized :
                                    height[proc] &amp;lt;= Len(b) &#x2F;\
                                    Epoch(b) &amp;lt;= epoch
                            },
23                          tx \in Tx,
                            b = Append(parent, &amp;lt;&amp;lt;epoch, tx&amp;gt;&amp;gt;)
                        ) {
24                          \* After the first synchronous epoch, the leader can pick a highest notarized block:
25                          when epoch &amp;gt; GSE =&amp;gt;
                                \A b2 \in Notarized :
                                    Len(b2) &amp;lt;= Len(parent);
26                          proposal := b
27                      };
28                  \* next, if possible, vote for the leader&amp;#39;s proposal:
29                  either if (height[proc] &amp;lt;= Len(proposal)-1) {
30                      votes[proc] := @ \cup {proposal};
31                      height[proc] := Len(proposal)-1
32                  }
33                  or {
34                      when epoch &amp;lt; GSE; \* Before GSE, we may miss the leader&amp;#39;s proposal
35                      skip
36                  };
37                  \* go to the next epoch if all processes have been scheduled:
38                  if (scheduled \cup {proc} = P) {
39                      scheduled := {};
40                      epoch := epoch+1
41                  }
42                  else
43                      scheduled := scheduled \cup {proc}
44              }
45          }
46      }
47  }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;model-checking-results-1&quot;&gt;Model-checking results&lt;&#x2F;h3&gt;
&lt;p&gt;I was able to exhaustively check the liveness property with 3 crash-stop processes, 2 block payloads, and 9 epochs among which the first 5 are asynchronous while the remaining 4 are synchronous (i.e. “GST” happens before epoch 6).&lt;&#x2F;p&gt;
&lt;p&gt;As before, this was done on a 24 core &lt;code&gt;Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz&lt;&#x2F;code&gt; with 40GB of memory reserved for TLC.
It took one hour and four minutes to complete and TLC found 320,821,303 distinct states for a depth of 29 steps.&lt;&#x2F;p&gt;
&lt;p&gt;I was also able to exhaustively check safety for 3 processes, 2 payloads, and 7 asynchronous epochs.
This took 16 hours and TLC found 3,262,833,142 distinct states for a depth of 23 steps.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;related-work&quot;&gt;Related work&lt;&#x2F;h1&gt;
&lt;p&gt;There is another excellent PlusCal&#x2F;TLA+ specification of the crash-fault Streamlet algorithm &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;muratbuffalo.blogspot.com&#x2F;2020&#x2F;07&#x2F;modeling-streamlet-in-tla.html&quot;&gt;described in Murat’s blog&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Compared to the present specification, this earlier specification uses a shared-whiteboard model of messages in which all processes receive a given message at the same time.
This means that processes always have the same view of the system, which precludes some interesting behaviors of the Streamlet algorithm.
In contrast, the specifications that I present reflect the fact that processes have different, partial views of what blocks have been notarized or not.&lt;&#x2F;p&gt;
&lt;p&gt;Shir Cohen and Dahlia Malkhi compare Streamlet and HotStuff in the following &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dahliamalkhi.github.io&#x2F;posts&#x2F;2020&#x2F;12&#x2F;what-they-didnt-teach-you-in-streamlet&#x2F;&quot;&gt;blog post&lt;&#x2F;a&gt;.
They note that Streamlet lacks some of the qualities of an engineering-ready protocol like HotStuff.
While their blog post is very interesting, I do not agree with the claim that Streamlet requires synchronized epochs.
The original Streamlet presentation indeed stipulates that processes proceeds through epochs in lock-steps (using synchronized real-time clocks). However, any synchronizer that guarantees that epochs become synchronous after GST
(in the sense that we have used in the current post) should work too.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;other-notes&quot;&gt;Other notes&lt;&#x2F;h1&gt;
&lt;p&gt;The rule that the leader uses to pick a block to extend can be slightly improved.
In the original Streamlet, the leader proposes a new block that extends one of the longest notarized chains that the leader knows of.
However, it would make some executions finalize a new block faster if the leader would instead pick the notarized block with the highest epoch that it knows of.&lt;&#x2F;p&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-note1&quot;&gt;
&lt;p&gt;this deviates slightly from the original formulation and makes the specification simpler without any downsides &lt;a href=&quot;#fr-note1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</content>
        
      </entry>
    
  
</feed>
