github.com/aakash4dev/cometbft@v0.38.2/spec/ivy-proofs/tendermint.ivy (about)

     1  #lang ivy1.7
     2  # ---
     3  # layout: page
     4  # title: Specification of Tendermint in Ivy
     5  # ---
     6  
     7  # This specification closely follows the pseudo-code given in "The latest
     8  # gossip on BFT consensus" by E. Buchman, J. Kwon, Z. Milosevic
     9  # <https://arxiv.org/abs/1807.04938>
    10  
    11  include domain_model
    12  include network_shim
    13  
    14  # We model the Tendermint protocol as an Ivy object. Like in Object-Oriented
    15  # Programming, the basic structuring unit in Ivy is the object. Objects have
    16  # internal state and actions (i.e. methods in OO parlance) that modify their
    17  # state. We model Tendermint as an object whose actions represent steps taken
    18  # by individual nodes in the protocol. Actions in Ivy can have preconditions,
    19  # and a valid execution is a sequence of actions whose preconditions are all
    20  # satisfied in the state in which they are called.
    21  
    22  # For technical reasons, we define below a `tendermint` module instead of an
    23  # object. Ivy modules are a little bit like classes in OO programs, and like
    24  # classes they can be instantiated to obtain objects. To instantiate the
    25  # `tendermint` module, we must provide an abstract-protocol object. This allows
    26  # us to use different abstract-protocol objects for different parts of the
    27  # proof, and to do so without too much notational burden (we could have used
    28  # Ivy monitors, but then we would need to prefix every variable name by the
    29  # name of the object containing it, which clutters things a bit compared to the
    30  # approach we took).
    31  
    32  # The abstract-protocol object is called by the resulting tendermint object so
    33  # as to run the abstract protocol alongside the concrete protocol. This allows
    34  # us to transfer properties proved of the abstract protocol to the concrete
    35  # protocol, as follows. First, we prove that running the abstract protocol in
    36  # this way results in a valid execution of the abstract protocol. This is done
    37  # by checking that all preconditions of the abstract actions are satisfied at
    38  # their call sites. Second, we establish a relation between abstract state and
    39  # concrete state (in the form of invariants of the resulting, two-object
    40  # transition system) that allow us to transfer properties proved in the
    41  # abstract protocol to the concrete protocol (for example, we prove that any
    42  # decision made in the Tendermint protocol is also made in the abstract
    43  # protocol; if the abstract protocol satisfies the agreement property, this
    44  # allows us to conclude that the Tendermint protocol also does).
    45  
    46  # The abstract protocol object that we will use is always the same, and only
    47  # the abstract properties that we prove about it change in the different
    48  # instantiations of the `tendermint` module. Thus we provide common invariants
    49  # that a) allow to prove that the abstract preconditions are met, and b)
    50  # provide a refinement relation (see end of the module) relating the state of
    51  # Tendermint to the state of the abstract protocol.
    52  
    53  # In the model, Byzantine nodes can send whatever messages they want, except
    54  # that they cannot forge sender identities. This reflects the fact that, in
    55  # practice, nodes use public key cryptography to sign their messages.
    56  
    57  # Finally, note that the observations that serve to adjudicate misbehavior are
    58  # defined only in the abstract protocol (they happen in the abstract actions).
    59  
    60  module tendermint(abstract_protocol) = {
    61  
    62      # the initial value of a node:
    63      function init_val(N:node): value
    64  
    65      # the three type of steps
    66      object step_t = {
    67          type this = {propose, prevote, precommit}
    68      } # refer to those e.g. as step_t.propose
    69  
    70      object server(n:node) = {
    71  
    72          # the current round of a node
    73          individual round_p: round
    74  
    75          individual step: step_t
    76  
    77          individual decision: value
    78  
    79          individual lockedValue: value
    80          individual lockedRound: round
    81  
    82          individual validValue: value
    83          individual validRound: round
    84  
    85  
    86          relation done_l34(R:round)
    87          relation done_l36(R:round, V:value)
    88          relation done_l47(R:round)
    89  
    90          # variables for scheduling request
    91          relation propose_timer_scheduled(R:round)
    92          relation prevote_timer_scheduled(R:round)
    93          relation precommit_timer_scheduled(R:round)
    94  
    95          relation _recved_proposal(Sender:node, R:round, V:value, VR:round)
    96          relation _recved_prevote(Sender:node, R:round, V:value)
    97          relation _recved_precommit(Sender:node, R:round, V:value)
    98  
    99          relation _has_started
   100  
   101          after init {
   102              round_p := 0;
   103              step := step_t.propose;
   104              decision := value.nil;
   105  
   106              lockedValue := value.nil;
   107              lockedRound := round.minus_one;
   108  
   109              validValue := value.nil;
   110              validRound := round.minus_one;
   111  
   112              done_l34(R) := false;
   113              done_l36(R, V) := false;
   114              done_l47(R) := false;
   115  
   116              propose_timer_scheduled(R) := false;
   117              prevote_timer_scheduled(R) := false;
   118              precommit_timer_scheduled(R) := false;
   119  
   120              _recved_proposal(Sender, R, V, VR) := false;
   121              _recved_prevote(Sender, R, V) := false;
   122              _recved_precommit(Sender, R, V) := false;
   123  
   124              _has_started := false;
   125          }
   126  
   127          action getValue returns (v:value) = {
   128              v := init_val(n)
   129          }
   130  
   131          export action start = {
   132              require ~_has_started;
   133              _has_started := true;
   134              # line 10
   135              call startRound(0);
   136          }
   137  
   138          # line 11-21
   139          action startRound(r:round) = {
   140              # line 12
   141              round_p := r;
   142  
   143              # line 13
   144              step := step_t.propose;
   145  
   146              var proposal : value;
   147  
   148              # line 14
   149              if (proposers.get_proposer(r) = n) {
   150                  if validValue ~= value.nil {      # line 15
   151                      proposal := validValue; # line 16
   152                  } else {
   153                      proposal := getValue();   # line 18
   154                  };
   155                  call broadcast_proposal(r, proposal, validRound); # line 19
   156              } else {
   157                  propose_timer_scheduled(r) := true; # line 21
   158              };
   159  
   160              call abstract_protocol.l_11(n, r);
   161          }
   162  
   163          # This action, as not exported, can only be called at specific call sites.
   164          action broadcast_proposal(r:round, v:value, vr:round) = {
   165              var m: msg;
   166              m.m_kind := msg_kind.proposal;
   167              m.m_src := n;
   168              m.m_round := r;
   169              m.m_value := v;
   170              m.m_vround := vr;
   171              call shim.broadcast(n,m);
   172          }
   173  
   174          implement shim.proposal_handler.handle(msg:msg) {
   175              _recved_proposal(msg.m_src, msg.m_round, msg.m_value, msg.m_vround) := true;
   176          }
   177  
   178          # line 22-27
   179          export action l_22(v:value) = {
   180              require _has_started;
   181              require _recved_proposal(proposers.get_proposer(round_p), round_p, v, round.minus_one);
   182              require step = step_t.propose;
   183  
   184              if (value.valid(v) & (lockedRound = round.minus_one | lockedValue = v)) {
   185                  call broadcast_prevote(round_p, v); # line 24
   186                  call abstract_protocol.l_22(n, round_p, v);
   187              } else {
   188                  call broadcast_prevote(round_p, value.nil); # line 26
   189                  call abstract_protocol.l_22(n, round_p, value.nil);
   190              };
   191  
   192              # line 27
   193              step := step_t.prevote;
   194          }
   195  
   196          # line 28-33
   197          export action l_28(r:round, v:value, vr:round, q:nset) = {
   198              require _has_started;
   199              require r = round_p;
   200              require _recved_proposal(proposers.get_proposer(r), r, v, vr);
   201              require nset.is_quorum(q);
   202              require nset.member(N,q) -> _recved_prevote(N,vr,v);
   203              require step = step_t.propose;
   204              require vr >= 0 & vr < r;
   205  
   206              # line 29
   207              if (value.valid(v) & (lockedRound <= vr | lockedValue = v)) {
   208                  call broadcast_prevote(r, v);
   209              } else {
   210                  call broadcast_prevote(r, value.nil);
   211              };
   212  
   213              call abstract_protocol.l_28(n,r,v,vr,q);
   214              step := step_t.prevote;
   215          }
   216  
   217          action broadcast_prevote(r:round, v:value) = {
   218              var m: msg;
   219              m.m_kind := msg_kind.prevote;
   220              m.m_src := n;
   221              m.m_round := r;
   222              m.m_value := v;
   223              call shim.broadcast(n,m);
   224          }
   225  
   226          implement shim.prevote_handler.handle(msg:msg) {
   227              _recved_prevote(msg.m_src, msg.m_round, msg.m_value) := true;
   228          }
   229  
   230          # line 34-35
   231          export action l_34(r:round, q:nset) = {
   232              require _has_started;
   233              require round_p = r;
   234              require nset.is_quorum(q);
   235              require exists V . nset.member(N,q) -> _recved_prevote(N,r,V);
   236              require step = step_t.prevote;
   237              require ~done_l34(r);
   238              done_l34(r) := true;
   239  
   240              prevote_timer_scheduled(r) := true;
   241          }
   242  
   243  
   244          # line 36-43
   245          export action l_36(r:round, v:value, q:nset) = {
   246              require _has_started;
   247              require r = round_p;
   248              require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR);
   249              require nset.is_quorum(q);
   250              require nset.member(N,q) -> _recved_prevote(N,r,v);
   251              require value.valid(v);
   252              require step = step_t.prevote | step = step_t.precommit;
   253  
   254              require ~done_l36(r,v);
   255              done_l36(r, v) := true;
   256  
   257              if step = step_t.prevote {
   258                  lockedValue := v; # line 38
   259                  lockedRound := r; # line 39
   260                  call broadcast_precommit(r, v); # line 40
   261                  step := step_t.precommit; # line 41
   262                  call abstract_protocol.l_36(n, r, v, q);
   263              };
   264  
   265              validValue := v; # line 42
   266              validRound := r; # line 43
   267          }
   268  
   269          # line 44-46
   270          export action l_44(r:round, q:nset) = {
   271              require _has_started;
   272              require r = round_p;
   273              require nset.is_quorum(q);
   274              require nset.member(N,q) -> _recved_prevote(N,r,value.nil);
   275              require step = step_t.prevote;
   276  
   277              call broadcast_precommit(r, value.nil); # line 45
   278              step := step_t.precommit; # line 46
   279  
   280              call abstract_protocol.l_44(n, r, q);
   281          }
   282  
   283          action broadcast_precommit(r:round, v:value) = {
   284              var m: msg;
   285              m.m_kind := msg_kind.precommit;
   286              m.m_src := n;
   287              m.m_round := r;
   288              m.m_value := v;
   289              call shim.broadcast(n,m);
   290          }
   291  
   292          implement shim.precommit_handler.handle(msg:msg) {
   293              _recved_precommit(msg.m_src, msg.m_round, msg.m_value) := true;
   294          }
   295  
   296  
   297          # line 47-48
   298          export action l_47(r:round, q:nset) = {
   299              require _has_started;
   300              require round_p = r;
   301              require nset.is_quorum(q);
   302              require nset.member(N,q) -> exists V . _recved_precommit(N,r,V);
   303              require ~done_l47(r);
   304              done_l47(r) := true;
   305  
   306              precommit_timer_scheduled(r) := true;
   307          }
   308  
   309  
   310          # line 49-54
   311          export action l_49_decide(r:round, v:value, q:nset) = {
   312              require _has_started;
   313              require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR);
   314              require nset.is_quorum(q);
   315              require nset.member(N,q) -> _recved_precommit(N,r,v);
   316              require decision = value.nil;
   317  
   318              if value.valid(v) {
   319                  decision := v;
   320                  # MORE for next height
   321                  call abstract_protocol.decide(n, r, v, q);
   322              }
   323          }
   324  
   325          # line 55-56
   326          export action l_55(r:round, b:nset) = {
   327              require _has_started;
   328              require nset.is_blocking(b);
   329              require nset.member(N,b) -> exists VR . round.minus_one <= VR & VR < r & exists V . _recved_proposal(N,r,V,VR) | _recved_prevote(N,r,V) | _recved_precommit(N,r,V);
   330              require r > round_p;
   331              call startRound(r); # line 56
   332          }
   333  
   334          # line 57-60
   335          export action onTimeoutPropose(r:round) = {
   336              require _has_started;
   337              require propose_timer_scheduled(r);
   338              require r = round_p;
   339              require step = step_t.propose;
   340              call broadcast_prevote(r,value.nil);
   341              step := step_t.prevote;
   342  
   343              call abstract_protocol.l_57(n,r);
   344  
   345              propose_timer_scheduled(r) := false;
   346          }
   347  
   348          # line 61-64
   349          export action onTimeoutPrevote(r:round) = {
   350              require _has_started;
   351              require prevote_timer_scheduled(r);
   352              require r = round_p;
   353              require step = step_t.prevote;
   354              call broadcast_precommit(r,value.nil);
   355              step := step_t.precommit;
   356  
   357              call abstract_protocol.l_61(n,r);
   358  
   359              prevote_timer_scheduled(r) := false;
   360          }
   361  
   362          # line 65-67
   363          export action onTimeoutPrecommit(r:round) = {
   364              require _has_started;
   365              require precommit_timer_scheduled(r);
   366              require r = round_p;
   367              call startRound(round.incr(r));
   368  
   369              precommit_timer_scheduled(r) := false;
   370          }
   371  
   372  # The Byzantine actions
   373  # ---------------------
   374  
   375  # Byzantine nodes can send whatever they want, but they cannot send
   376  # messages on behalf of well-behaved nodes. In practice this is implemented
   377  # using cryptography (e.g. public-key cryptography).
   378  
   379          export action byzantine_send(m:msg, dst:node) = {
   380              require ~well_behaved(n);
   381              require ~well_behaved(m.m_src); # cannot forge the identity of well-behaved nodes
   382              call shim.send(n,dst,m);
   383          }
   384  
   385  # Byzantine nodes can also report fake observations, as defined in the abstract protocol.
   386          export action fake_observations = {
   387              call abstract_protocol.misbehave
   388          }
   389  
   390  # Invariants
   391  # ----------
   392  
   393  # We provide common invariants that a) allow to prove that the abstract
   394  # preconditions are met, and b) provide a refinement relation.
   395  
   396  
   397          specification {
   398  
   399              invariant 0 <= round_p
   400              invariant abstract_protocol.left_round(n,R) <-> R < round_p
   401  
   402              invariant lockedRound ~= round.minus_one -> forall R,V . abstract_protocol.locked(n,R,V) <-> R <= lockedRound & lockedValue = V
   403              invariant lockedRound = round.minus_one -> forall R,V . ~abstract_protocol.locked(n,R,V)
   404  
   405              invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.prevote & shim.sent(M,N) -> abstract_protocol.prevoted(M.m_src,M.m_round,M.m_value)
   406              invariant well_behaved(N) & _recved_prevote(N,R,V) -> abstract_protocol.prevoted(N,R,V)
   407              invariant forall M:msg . well_behaved(M.m_src) & M.m_kind = msg_kind.precommit & shim.sent(M,N) -> abstract_protocol.precommitted(M.m_src,M.m_round,M.m_value)
   408              invariant well_behaved(N) & _recved_precommit(N,R,V) -> abstract_protocol.precommitted(N,R,V)
   409  
   410              invariant (step = step_t.prevote | step = step_t.propose) -> ~abstract_protocol.precommitted(n,round_p,V)
   411              invariant step = step_t.propose -> ~abstract_protocol.prevoted(n,round_p,V)
   412              invariant step = step_t.prevote -> exists V . abstract_protocol.prevoted(n,round_p,V)
   413  
   414              invariant round_p < R -> ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V))
   415              invariant ~_has_started -> step = step_t.propose & ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) & round_p = 0
   416  
   417              invariant decision ~= value.nil -> exists R . abstract_protocol.decided(n,R,decision)
   418          }
   419      }
   420  }