#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
# <https://arxiv.org/abs/1807.04938>

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)
        }
    }
}
