#lang ivy1.7 # --- # layout: page # title: Specification of Tendermint in Ivy # --- # This specification closely follows the pseudo-code given in "The latest # gossip on BFT consensus" by E. Buchman, J. Kwon, Z. Milosevic # include domain_model include network_shim # We model the Tendermint protocol as an Ivy object. Like in Object-Oriented # Programming, the basic structuring unit in Ivy is the object. Objects have # internal state and actions (i.e. methods in OO parlance) that modify their # state. We model Tendermint as an object whose actions represent steps taken # by individual nodes in the protocol. Actions in Ivy can have preconditions, # and a valid execution is a sequence of actions whose preconditions are all # satisfied in the state in which they are called. # For technical reasons, we define below a `tendermint` module instead of an # object. Ivy modules are a little bit like classes in OO programs, and like # classes they can be instantiated to obtain objects. To instantiate the # `tendermint` module, we must provide an abstract-protocol object. This allows # us to use different abstract-protocol objects for different parts of the # proof, and to do so without too much notational burden (we could have used # Ivy monitors, but then we would need to prefix every variable name by the # name of the object containing it, which clutters things a bit compared to the # approach we took). # The abstract-protocol object is called by the resulting tendermint object so # as to run the abstract protocol alongside the concrete protocol. This allows # us to transfer properties proved of the abstract protocol to the concrete # protocol, as follows. First, we prove that running the abstract protocol in # this way results in a valid execution of the abstract protocol. This is done # by checking that all preconditions of the abstract actions are satisfied at # their call sites. Second, we establish a relation between abstract state and # concrete state (in the form of invariants of the resulting, two-object # transition system) that allow us to transfer properties proved in the # abstract protocol to the concrete protocol (for example, we prove that any # decision made in the Tendermint protocol is also made in the abstract # protocol; if the abstract protocol satisfies the agreement property, this # allows us to conclude that the Tendermint protocol also does). # The abstract protocol object that we will use is always the same, and only # the abstract properties that we prove about it change in the different # instantiations of the `tendermint` module. Thus we provide common invariants # that a) allow to prove that the abstract preconditions are met, and b) # provide a refinement relation (see end of the module) relating the state of # Tendermint to the state of the abstract protocol. # In the model, Byzantine nodes can send whatever messages they want, except # that they cannot forge sender identities. This reflects the fact that, in # practice, nodes use public key cryptography to sign their messages. # Finally, note that the observations that serve to adjudicate misbehavior are # defined only in the abstract protocol (they happen in the abstract actions). module tendermint(abstract_protocol) = { # the initial value of a node: function init_val(N:node): value # the three type of steps object step_t = { type this = {propose, prevote, precommit} } # refer to those e.g. as step_t.propose object server(n:node) = { # the current round of a node individual round_p: round individual step: step_t individual decision: value individual lockedValue: value individual lockedRound: round individual validValue: value individual validRound: round relation done_l34(R:round) relation done_l36(R:round, V:value) relation done_l47(R:round) # variables for scheduling request relation propose_timer_scheduled(R:round) relation prevote_timer_scheduled(R:round) relation precommit_timer_scheduled(R:round) relation _recved_proposal(Sender:node, R:round, V:value, VR:round) relation _recved_prevote(Sender:node, R:round, V:value) relation _recved_precommit(Sender:node, R:round, V:value) relation _has_started after init { round_p := 0; step := step_t.propose; decision := value.nil; lockedValue := value.nil; lockedRound := round.minus_one; validValue := value.nil; validRound := round.minus_one; done_l34(R) := false; done_l36(R, V) := false; done_l47(R) := false; propose_timer_scheduled(R) := false; prevote_timer_scheduled(R) := false; precommit_timer_scheduled(R) := false; _recved_proposal(Sender, R, V, VR) := false; _recved_prevote(Sender, R, V) := false; _recved_precommit(Sender, R, V) := false; _has_started := false; } action getValue returns (v:value) = { v := init_val(n) } export action start = { require ~_has_started; _has_started := true; # line 10 call startRound(0); } # line 11-21 action startRound(r:round) = { # line 12 round_p := r; # line 13 step := step_t.propose; var proposal : value; # line 14 if (proposers.get_proposer(r) = n) { if validValue ~= value.nil { # line 15 proposal := validValue; # line 16 } else { proposal := getValue(); # line 18 }; call broadcast_proposal(r, proposal, validRound); # line 19 } else { propose_timer_scheduled(r) := true; # line 21 }; call abstract_protocol.l_11(n, r); } # This action, as not exported, can only be called at specific call sites. action broadcast_proposal(r:round, v:value, vr:round) = { var m: msg; m.m_kind := msg_kind.proposal; m.m_src := n; m.m_round := r; m.m_value := v; m.m_vround := vr; call shim.broadcast(n,m); } implement shim.proposal_handler.handle(msg:msg) { _recved_proposal(msg.m_src, msg.m_round, msg.m_value, msg.m_vround) := true; } # line 22-27 export action l_22(v:value) = { require _has_started; require _recved_proposal(proposers.get_proposer(round_p), round_p, v, round.minus_one); require step = step_t.propose; if (value.valid(v) & (lockedRound = round.minus_one | lockedValue = v)) { call broadcast_prevote(round_p, v); # line 24 call abstract_protocol.l_22(n, round_p, v); } else { call broadcast_prevote(round_p, value.nil); # line 26 call abstract_protocol.l_22(n, round_p, value.nil); }; # line 27 step := step_t.prevote; } # line 28-33 export action l_28(r:round, v:value, vr:round, q:nset) = { require _has_started; require r = round_p; require _recved_proposal(proposers.get_proposer(r), r, v, vr); require nset.is_quorum(q); require nset.member(N,q) -> _recved_prevote(N,vr,v); require step = step_t.propose; require vr >= 0 & vr < r; # line 29 if (value.valid(v) & (lockedRound <= vr | lockedValue = v)) { call broadcast_prevote(r, v); } else { call broadcast_prevote(r, value.nil); }; call abstract_protocol.l_28(n,r,v,vr,q); step := step_t.prevote; } action broadcast_prevote(r:round, v:value) = { var m: msg; m.m_kind := msg_kind.prevote; m.m_src := n; m.m_round := r; m.m_value := v; call shim.broadcast(n,m); } implement shim.prevote_handler.handle(msg:msg) { _recved_prevote(msg.m_src, msg.m_round, msg.m_value) := true; } # line 34-35 export action l_34(r:round, q:nset) = { require _has_started; require round_p = r; require nset.is_quorum(q); require exists V . nset.member(N,q) -> _recved_prevote(N,r,V); require step = step_t.prevote; require ~done_l34(r); done_l34(r) := true; prevote_timer_scheduled(r) := true; } # line 36-43 export action l_36(r:round, v:value, q:nset) = { require _has_started; require r = round_p; require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); require nset.is_quorum(q); require nset.member(N,q) -> _recved_prevote(N,r,v); require value.valid(v); require step = step_t.prevote | step = step_t.precommit; require ~done_l36(r,v); done_l36(r, v) := true; if step = step_t.prevote { lockedValue := v; # line 38 lockedRound := r; # line 39 call broadcast_precommit(r, v); # line 40 step := step_t.precommit; # line 41 call abstract_protocol.l_36(n, r, v, q); }; validValue := v; # line 42 validRound := r; # line 43 } # line 44-46 export action l_44(r:round, q:nset) = { require _has_started; require r = round_p; require nset.is_quorum(q); require nset.member(N,q) -> _recved_prevote(N,r,value.nil); require step = step_t.prevote; call broadcast_precommit(r, value.nil); # line 45 step := step_t.precommit; # line 46 call abstract_protocol.l_44(n, r, q); } action broadcast_precommit(r:round, v:value) = { var m: msg; m.m_kind := msg_kind.precommit; m.m_src := n; m.m_round := r; m.m_value := v; call shim.broadcast(n,m); } implement shim.precommit_handler.handle(msg:msg) { _recved_precommit(msg.m_src, msg.m_round, msg.m_value) := true; } # line 47-48 export action l_47(r:round, q:nset) = { require _has_started; require round_p = r; require nset.is_quorum(q); require nset.member(N,q) -> exists V . _recved_precommit(N,r,V); require ~done_l47(r); done_l47(r) := true; precommit_timer_scheduled(r) := true; } # line 49-54 export action l_49_decide(r:round, v:value, q:nset) = { require _has_started; require exists VR . round.minus_one <= VR & VR < r & _recved_proposal(proposers.get_proposer(r), r, v, VR); require nset.is_quorum(q); require nset.member(N,q) -> _recved_precommit(N,r,v); require decision = value.nil; if value.valid(v) { decision := v; # MORE for next height call abstract_protocol.decide(n, r, v, q); } } # line 55-56 export action l_55(r:round, b:nset) = { require _has_started; require nset.is_blocking(b); 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); require r > round_p; call startRound(r); # line 56 } # line 57-60 export action onTimeoutPropose(r:round) = { require _has_started; require propose_timer_scheduled(r); require r = round_p; require step = step_t.propose; call broadcast_prevote(r,value.nil); step := step_t.prevote; call abstract_protocol.l_57(n,r); propose_timer_scheduled(r) := false; } # line 61-64 export action onTimeoutPrevote(r:round) = { require _has_started; require prevote_timer_scheduled(r); require r = round_p; require step = step_t.prevote; call broadcast_precommit(r,value.nil); step := step_t.precommit; call abstract_protocol.l_61(n,r); prevote_timer_scheduled(r) := false; } # line 65-67 export action onTimeoutPrecommit(r:round) = { require _has_started; require precommit_timer_scheduled(r); require r = round_p; call startRound(round.incr(r)); precommit_timer_scheduled(r) := false; } # The Byzantine actions # --------------------- # Byzantine nodes can send whatever they want, but they cannot send # messages on behalf of well-behaved nodes. In practice this is implemented # using cryptography (e.g. public-key cryptography). export action byzantine_send(m:msg, dst:node) = { require ~well_behaved(n); require ~well_behaved(m.m_src); # cannot forge the identity of well-behaved nodes call shim.send(n,dst,m); } # Byzantine nodes can also report fake observations, as defined in the abstract protocol. export action fake_observations = { call abstract_protocol.misbehave } # Invariants # ---------- # We provide common invariants that a) allow to prove that the abstract # preconditions are met, and b) provide a refinement relation. specification { invariant 0 <= round_p invariant abstract_protocol.left_round(n,R) <-> R < round_p invariant lockedRound ~= round.minus_one -> forall R,V . abstract_protocol.locked(n,R,V) <-> R <= lockedRound & lockedValue = V invariant lockedRound = round.minus_one -> forall R,V . ~abstract_protocol.locked(n,R,V) 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) invariant well_behaved(N) & _recved_prevote(N,R,V) -> abstract_protocol.prevoted(N,R,V) 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) invariant well_behaved(N) & _recved_precommit(N,R,V) -> abstract_protocol.precommitted(N,R,V) invariant (step = step_t.prevote | step = step_t.propose) -> ~abstract_protocol.precommitted(n,round_p,V) invariant step = step_t.propose -> ~abstract_protocol.prevoted(n,round_p,V) invariant step = step_t.prevote -> exists V . abstract_protocol.prevoted(n,round_p,V) invariant round_p < R -> ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) invariant ~_has_started -> step = step_t.propose & ~(abstract_protocol.prevoted(n,R,V) | abstract_protocol.precommitted(n,R,V)) & round_p = 0 invariant decision ~= value.nil -> exists R . abstract_protocol.decided(n,R,decision) } } }