You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

420 lines
15 KiB

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