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.

430 lines
15 KiB

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