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 }