github.com/Team-Kujira/tendermint@v0.34.24-indexer/spec/light-client/accountability/TendermintAcc_004_draft.tla (about) 1 -------------------- MODULE TendermintAcc_004_draft --------------------------- 2 (* 3 A TLA+ specification of a simplified Tendermint consensus, tuned for 4 fork accountability. The simplifications are as follows: 5 6 - the protocol runs for one height, that is, it is one-shot consensus 7 8 - this specification focuses on safety, so timeouts are modelled 9 with non-determinism 10 11 - the proposer function is non-determinstic, no fairness is assumed 12 13 - the messages by the faulty processes are injected right in the initial states 14 15 - every process has the voting power of 1 16 17 - hashes are modelled as identity 18 19 Having the above assumptions in mind, the specification follows the pseudo-code 20 of the Tendermint paper: https://arxiv.org/abs/1807.04938 21 22 Byzantine processes can demonstrate arbitrary behavior, including 23 no communication. We show that if agreement is violated, then the Byzantine 24 processes demonstrate one of the two behaviours: 25 26 - Equivocation: a Byzantine process may send two different values 27 in the same round. 28 29 - Amnesia: a Byzantine process may lock a value without unlocking 30 the previous value that it has locked in the past. 31 32 * Version 4. Remove defective processes, fix bugs, collect global evidence. 33 * Version 3. Modular and parameterized definitions. 34 * Version 2. Bugfixes in the spec and an inductive invariant. 35 * Version 1. A preliminary specification. 36 37 Zarko Milosevic, Igor Konnov, Informal Systems, 2019-2020. 38 *) 39 40 EXTENDS Integers, FiniteSets 41 42 (********************* PROTOCOL PARAMETERS **********************************) 43 CONSTANTS 44 Corr, \* the set of correct processes 45 Faulty, \* the set of Byzantine processes, may be empty 46 N, \* the total number of processes: correct, defective, and Byzantine 47 T, \* an upper bound on the number of Byzantine processes 48 ValidValues, \* the set of valid values, proposed both by correct and faulty 49 InvalidValues, \* the set of invalid values, never proposed by the correct ones 50 MaxRound, \* the maximal round number 51 Proposer \* the proposer function from 0..NRounds to 1..N 52 53 ASSUME(N = Cardinality(Corr \union Faulty)) 54 55 (*************************** DEFINITIONS ************************************) 56 AllProcs == Corr \union Faulty \* the set of all processes 57 Rounds == 0..MaxRound \* the set of potential rounds 58 NilRound == -1 \* a special value to denote a nil round, outside of Rounds 59 RoundsOrNil == Rounds \union {NilRound} 60 Values == ValidValues \union InvalidValues \* the set of all values 61 NilValue == "None" \* a special value for a nil round, outside of Values 62 ValuesOrNil == Values \union {NilValue} 63 64 \* a value hash is modeled as identity 65 Id(v) == v 66 67 \* The validity predicate 68 IsValid(v) == v \in ValidValues 69 70 \* the two thresholds that are used in the algorithm 71 THRESHOLD1 == T + 1 \* at least one process is not faulty 72 THRESHOLD2 == 2 * T + 1 \* a quorum when having N > 3 * T 73 74 (********************* TYPE ANNOTATIONS FOR APALACHE **************************) 75 \* the operator for type annotations 76 a <: b == a 77 78 \* the type of message records 79 MT == [type |-> STRING, src |-> STRING, round |-> Int, 80 proposal |-> STRING, validRound |-> Int, id |-> STRING] 81 82 \* a type annotation for a message 83 AsMsg(m) == m <: MT 84 \* a type annotation for a set of messages 85 SetOfMsgs(S) == S <: {MT} 86 \* a type annotation for an empty set of messages 87 EmptyMsgSet == SetOfMsgs({}) 88 89 (********************* PROTOCOL STATE VARIABLES ******************************) 90 VARIABLES 91 round, \* a process round number: Corr -> Rounds 92 step, \* a process step: Corr -> { "PROPOSE", "PREVOTE", "PRECOMMIT", "DECIDED" } 93 decision, \* process decision: Corr -> ValuesOrNil 94 lockedValue, \* a locked value: Corr -> ValuesOrNil 95 lockedRound, \* a locked round: Corr -> RoundsOrNil 96 validValue, \* a valid value: Corr -> ValuesOrNil 97 validRound \* a valid round: Corr -> RoundsOrNil 98 99 \* book-keeping variables 100 VARIABLES 101 msgsPropose, \* PROPOSE messages broadcast in the system, Rounds -> Messages 102 msgsPrevote, \* PREVOTE messages broadcast in the system, Rounds -> Messages 103 msgsPrecommit, \* PRECOMMIT messages broadcast in the system, Rounds -> Messages 104 evidence, \* the messages that were used by the correct processes to make transitions 105 action \* we use this variable to see which action was taken 106 107 (* to see a type invariant, check TendermintAccInv3 *) 108 109 \* a handy definition used in UNCHANGED 110 vars == <<round, step, decision, lockedValue, lockedRound, 111 validValue, validRound, evidence, msgsPropose, msgsPrevote, msgsPrecommit>> 112 113 (********************* PROTOCOL INITIALIZATION ******************************) 114 FaultyProposals(r) == 115 SetOfMsgs([type: {"PROPOSAL"}, src: Faulty, 116 round: {r}, proposal: Values, validRound: RoundsOrNil]) 117 118 AllFaultyProposals == 119 SetOfMsgs([type: {"PROPOSAL"}, src: Faulty, 120 round: Rounds, proposal: Values, validRound: RoundsOrNil]) 121 122 FaultyPrevotes(r) == 123 SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: {r}, id: Values]) 124 125 AllFaultyPrevotes == 126 SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: Rounds, id: Values]) 127 128 FaultyPrecommits(r) == 129 SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: {r}, id: Values]) 130 131 AllFaultyPrecommits == 132 SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: Rounds, id: Values]) 133 134 BenignRoundsInMessages(msgfun) == 135 \* the message function never contains a message for a wrong round 136 \A r \in Rounds: 137 \A m \in msgfun[r]: 138 r = m.round 139 140 \* The initial states of the protocol. Some faults can be in the system already. 141 Init == 142 /\ round = [p \in Corr |-> 0] 143 /\ step = [p \in Corr |-> "PROPOSE"] 144 /\ decision = [p \in Corr |-> NilValue] 145 /\ lockedValue = [p \in Corr |-> NilValue] 146 /\ lockedRound = [p \in Corr |-> NilRound] 147 /\ validValue = [p \in Corr |-> NilValue] 148 /\ validRound = [p \in Corr |-> NilRound] 149 /\ msgsPropose \in [Rounds -> SUBSET AllFaultyProposals] 150 /\ msgsPrevote \in [Rounds -> SUBSET AllFaultyPrevotes] 151 /\ msgsPrecommit \in [Rounds -> SUBSET AllFaultyPrecommits] 152 /\ BenignRoundsInMessages(msgsPropose) 153 /\ BenignRoundsInMessages(msgsPrevote) 154 /\ BenignRoundsInMessages(msgsPrecommit) 155 /\ evidence = EmptyMsgSet 156 /\ action' = "Init" 157 158 (************************ MESSAGE PASSING ********************************) 159 BroadcastProposal(pSrc, pRound, pProposal, pValidRound) == 160 LET newMsg == 161 AsMsg([type |-> "PROPOSAL", src |-> pSrc, round |-> pRound, 162 proposal |-> pProposal, validRound |-> pValidRound]) 163 IN 164 msgsPropose' = [msgsPropose EXCEPT ![pRound] = msgsPropose[pRound] \union {newMsg}] 165 166 BroadcastPrevote(pSrc, pRound, pId) == 167 LET newMsg == AsMsg([type |-> "PREVOTE", 168 src |-> pSrc, round |-> pRound, id |-> pId]) 169 IN 170 msgsPrevote' = [msgsPrevote EXCEPT ![pRound] = msgsPrevote[pRound] \union {newMsg}] 171 172 BroadcastPrecommit(pSrc, pRound, pId) == 173 LET newMsg == AsMsg([type |-> "PRECOMMIT", 174 src |-> pSrc, round |-> pRound, id |-> pId]) 175 IN 176 msgsPrecommit' = [msgsPrecommit EXCEPT ![pRound] = msgsPrecommit[pRound] \union {newMsg}] 177 178 179 (********************* PROTOCOL TRANSITIONS ******************************) 180 \* lines 12-13 181 StartRound(p, r) == 182 /\ step[p] /= "DECIDED" \* a decided process does not participate in consensus 183 /\ round' = [round EXCEPT ![p] = r] 184 /\ step' = [step EXCEPT ![p] = "PROPOSE"] 185 186 \* lines 14-19, a proposal may be sent later 187 InsertProposal(p) == 188 LET r == round[p] IN 189 /\ p = Proposer[r] 190 /\ step[p] = "PROPOSE" 191 \* if the proposer is sending a proposal, then there are no other proposals 192 \* by the correct processes for the same round 193 /\ \A m \in msgsPropose[r]: m.src /= p 194 /\ \E v \in ValidValues: 195 LET proposal == IF validValue[p] /= NilValue THEN validValue[p] ELSE v IN 196 BroadcastProposal(p, round[p], proposal, validRound[p]) 197 /\ UNCHANGED <<evidence, round, decision, lockedValue, lockedRound, 198 validValue, step, validRound, msgsPrevote, msgsPrecommit>> 199 /\ action' = "InsertProposal" 200 201 \* lines 22-27 202 UponProposalInPropose(p) == 203 \E v \in Values: 204 /\ step[p] = "PROPOSE" (* line 22 *) 205 /\ LET msg == 206 AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], 207 round |-> round[p], proposal |-> v, validRound |-> NilRound]) IN 208 /\ msg \in msgsPropose[round[p]] \* line 22 209 /\ evidence' = {msg} \union evidence 210 /\ LET mid == (* line 23 *) 211 IF IsValid(v) /\ (lockedRound[p] = NilRound \/ lockedValue[p] = v) 212 THEN Id(v) 213 ELSE NilValue 214 IN 215 BroadcastPrevote(p, round[p], mid) \* lines 24-26 216 /\ step' = [step EXCEPT ![p] = "PREVOTE"] 217 /\ UNCHANGED <<round, decision, lockedValue, lockedRound, 218 validValue, validRound, msgsPropose, msgsPrecommit>> 219 /\ action' = "UponProposalInPropose" 220 221 \* lines 28-33 222 UponProposalInProposeAndPrevote(p) == 223 \E v \in Values, vr \in Rounds: 224 /\ step[p] = "PROPOSE" /\ 0 <= vr /\ vr < round[p] \* line 28, the while part 225 /\ LET msg == 226 AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], 227 round |-> round[p], proposal |-> v, validRound |-> vr]) 228 IN 229 /\ msg \in msgsPropose[round[p]] \* line 28 230 /\ LET PV == { m \in msgsPrevote[vr]: m.id = Id(v) } IN 231 /\ Cardinality(PV) >= THRESHOLD2 \* line 28 232 /\ evidence' = PV \union {msg} \union evidence 233 /\ LET mid == (* line 29 *) 234 IF IsValid(v) /\ (lockedRound[p] <= vr \/ lockedValue[p] = v) 235 THEN Id(v) 236 ELSE NilValue 237 IN 238 BroadcastPrevote(p, round[p], mid) \* lines 24-26 239 /\ step' = [step EXCEPT ![p] = "PREVOTE"] 240 /\ UNCHANGED <<round, decision, lockedValue, lockedRound, 241 validValue, validRound, msgsPropose, msgsPrecommit>> 242 /\ action' = "UponProposalInProposeAndPrevote" 243 244 \* lines 34-35 + lines 61-64 (onTimeoutPrevote) 245 UponQuorumOfPrevotesAny(p) == 246 /\ step[p] = "PREVOTE" \* line 34 and 61 247 /\ \E MyEvidence \in SUBSET msgsPrevote[round[p]]: 248 \* find the unique voters in the evidence 249 LET Voters == { m.src: m \in MyEvidence } IN 250 \* compare the number of the unique voters against the threshold 251 /\ Cardinality(Voters) >= THRESHOLD2 \* line 34 252 /\ evidence' = MyEvidence \union evidence 253 /\ BroadcastPrecommit(p, round[p], NilValue) 254 /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] 255 /\ UNCHANGED <<round, decision, lockedValue, lockedRound, 256 validValue, validRound, msgsPropose, msgsPrevote>> 257 /\ action' = "UponQuorumOfPrevotesAny" 258 259 \* lines 36-46 260 UponProposalInPrevoteOrCommitAndPrevote(p) == 261 \E v \in ValidValues, vr \in RoundsOrNil: 262 /\ step[p] \in {"PREVOTE", "PRECOMMIT"} \* line 36 263 /\ LET msg == 264 AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]], 265 round |-> round[p], proposal |-> v, validRound |-> vr]) IN 266 /\ msg \in msgsPropose[round[p]] \* line 36 267 /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(v) } IN 268 /\ Cardinality(PV) >= THRESHOLD2 \* line 36 269 /\ evidence' = PV \union {msg} \union evidence 270 /\ IF step[p] = "PREVOTE" 271 THEN \* lines 38-41: 272 /\ lockedValue' = [lockedValue EXCEPT ![p] = v] 273 /\ lockedRound' = [lockedRound EXCEPT ![p] = round[p]] 274 /\ BroadcastPrecommit(p, round[p], Id(v)) 275 /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] 276 ELSE 277 UNCHANGED <<lockedValue, lockedRound, msgsPrecommit, step>> 278 \* lines 42-43 279 /\ validValue' = [validValue EXCEPT ![p] = v] 280 /\ validRound' = [validRound EXCEPT ![p] = round[p]] 281 /\ UNCHANGED <<round, decision, msgsPropose, msgsPrevote>> 282 /\ action' = "UponProposalInPrevoteOrCommitAndPrevote" 283 284 \* lines 47-48 + 65-67 (onTimeoutPrecommit) 285 UponQuorumOfPrecommitsAny(p) == 286 /\ \E MyEvidence \in SUBSET msgsPrecommit[round[p]]: 287 \* find the unique committers in the evidence 288 LET Committers == { m.src: m \in MyEvidence } IN 289 \* compare the number of the unique committers against the threshold 290 /\ Cardinality(Committers) >= THRESHOLD2 \* line 47 291 /\ evidence' = MyEvidence \union evidence 292 /\ round[p] + 1 \in Rounds 293 /\ StartRound(p, round[p] + 1) 294 /\ UNCHANGED <<decision, lockedValue, lockedRound, validValue, 295 validRound, msgsPropose, msgsPrevote, msgsPrecommit>> 296 /\ action' = "UponQuorumOfPrecommitsAny" 297 298 \* lines 49-54 299 UponProposalInPrecommitNoDecision(p) == 300 /\ decision[p] = NilValue \* line 49 301 /\ \E v \in ValidValues (* line 50*) , r \in Rounds, vr \in RoundsOrNil: 302 /\ LET msg == AsMsg([type |-> "PROPOSAL", src |-> Proposer[r], 303 round |-> r, proposal |-> v, validRound |-> vr]) IN 304 /\ msg \in msgsPropose[r] \* line 49 305 /\ LET PV == { m \in msgsPrecommit[r]: m.id = Id(v) } IN 306 /\ Cardinality(PV) >= THRESHOLD2 \* line 49 307 /\ evidence' = PV \union {msg} \union evidence 308 /\ decision' = [decision EXCEPT ![p] = v] \* update the decision, line 51 309 \* The original algorithm does not have 'DECIDED', but it increments the height. 310 \* We introduced 'DECIDED' here to prevent the process from changing its decision. 311 /\ step' = [step EXCEPT ![p] = "DECIDED"] 312 /\ UNCHANGED <<round, lockedValue, lockedRound, validValue, 313 validRound, msgsPropose, msgsPrevote, msgsPrecommit>> 314 /\ action' = "UponProposalInPrecommitNoDecision" 315 316 \* the actions below are not essential for safety, but added for completeness 317 318 \* lines 20-21 + 57-60 319 OnTimeoutPropose(p) == 320 /\ step[p] = "PROPOSE" 321 /\ p /= Proposer[round[p]] 322 /\ BroadcastPrevote(p, round[p], NilValue) 323 /\ step' = [step EXCEPT ![p] = "PREVOTE"] 324 /\ UNCHANGED <<round, lockedValue, lockedRound, validValue, 325 validRound, decision, evidence, msgsPropose, msgsPrecommit>> 326 /\ action' = "OnTimeoutPropose" 327 328 \* lines 44-46 329 OnQuorumOfNilPrevotes(p) == 330 /\ step[p] = "PREVOTE" 331 /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(NilValue) } IN 332 /\ Cardinality(PV) >= THRESHOLD2 \* line 36 333 /\ evidence' = PV \union evidence 334 /\ BroadcastPrecommit(p, round[p], Id(NilValue)) 335 /\ step' = [step EXCEPT ![p] = "PRECOMMIT"] 336 /\ UNCHANGED <<round, lockedValue, lockedRound, validValue, 337 validRound, decision, msgsPropose, msgsPrevote>> 338 /\ action' = "OnQuorumOfNilPrevotes" 339 340 \* lines 55-56 341 OnRoundCatchup(p) == 342 \E r \in {rr \in Rounds: rr > round[p]}: 343 LET RoundMsgs == msgsPropose[r] \union msgsPrevote[r] \union msgsPrecommit[r] IN 344 \E MyEvidence \in SUBSET RoundMsgs: 345 LET Faster == { m.src: m \in MyEvidence } IN 346 /\ Cardinality(Faster) >= THRESHOLD1 347 /\ evidence' = MyEvidence \union evidence 348 /\ StartRound(p, r) 349 /\ UNCHANGED <<decision, lockedValue, lockedRound, validValue, 350 validRound, msgsPropose, msgsPrevote, msgsPrecommit>> 351 /\ action' = "OnRoundCatchup" 352 353 (* 354 * A system transition. In this specificatiom, the system may eventually deadlock, 355 * e.g., when all processes decide. This is expected behavior, as we focus on safety. 356 *) 357 Next == 358 \E p \in Corr: 359 \/ InsertProposal(p) 360 \/ UponProposalInPropose(p) 361 \/ UponProposalInProposeAndPrevote(p) 362 \/ UponQuorumOfPrevotesAny(p) 363 \/ UponProposalInPrevoteOrCommitAndPrevote(p) 364 \/ UponQuorumOfPrecommitsAny(p) 365 \/ UponProposalInPrecommitNoDecision(p) 366 \* the actions below are not essential for safety, but added for completeness 367 \/ OnTimeoutPropose(p) 368 \/ OnQuorumOfNilPrevotes(p) 369 \/ OnRoundCatchup(p) 370 371 372 (**************************** FORK SCENARIOS ***************************) 373 374 \* equivocation by a process p 375 EquivocationBy(p) == 376 \E m1, m2 \in evidence: 377 /\ m1 /= m2 378 /\ m1.src = p 379 /\ m2.src = p 380 /\ m1.round = m2.round 381 /\ m1.type = m2.type 382 383 \* amnesic behavior by a process p 384 AmnesiaBy(p) == 385 \E r1, r2 \in Rounds: 386 /\ r1 < r2 387 /\ \E v1, v2 \in ValidValues: 388 /\ v1 /= v2 389 /\ AsMsg([type |-> "PRECOMMIT", src |-> p, 390 round |-> r1, id |-> Id(v1)]) \in evidence 391 /\ AsMsg([type |-> "PREVOTE", src |-> p, 392 round |-> r2, id |-> Id(v2)]) \in evidence 393 /\ \A r \in { rnd \in Rounds: r1 <= rnd /\ rnd < r2 }: 394 LET prevotes == 395 { m \in evidence: 396 m.type = "PREVOTE" /\ m.round = r /\ m.id = Id(v2) } 397 IN 398 Cardinality(prevotes) < THRESHOLD2 399 400 (******************************** PROPERTIES ***************************************) 401 402 \* the safety property -- agreement 403 Agreement == 404 \A p, q \in Corr: 405 \/ decision[p] = NilValue 406 \/ decision[q] = NilValue 407 \/ decision[p] = decision[q] 408 409 \* the protocol validity 410 Validity == 411 \A p \in Corr: decision[p] \in ValidValues \union {NilValue} 412 413 (* 414 The protocol safety. Two cases are possible: 415 1. There is no fork, that is, Agreement holds true. 416 2. A subset of faulty processes demonstrates equivocation or amnesia. 417 *) 418 Accountability == 419 \/ Agreement 420 \/ \E Detectable \in SUBSET Faulty: 421 /\ Cardinality(Detectable) >= THRESHOLD1 422 /\ \A p \in Detectable: 423 EquivocationBy(p) \/ AmnesiaBy(p) 424 425 (****************** FALSE INVARIANTS TO PRODUCE EXAMPLES ***********************) 426 427 \* This property is violated. You can check it to see how amnesic behavior 428 \* appears in the evidence variable. 429 NoAmnesia == 430 \A p \in Faulty: ~AmnesiaBy(p) 431 432 \* This property is violated. You can check it to see an example of equivocation. 433 NoEquivocation == 434 \A p \in Faulty: ~EquivocationBy(p) 435 436 \* This property is violated. You can check it to see an example of agreement. 437 \* It is not exactly ~Agreement, as we do not want to see the states where 438 \* decision[p] = NilValue 439 NoAgreement == 440 \A p, q \in Corr: 441 (p /= q /\ decision[p] /= NilValue /\ decision[q] /= NilValue) 442 => decision[p] /= decision[q] 443 444 \* Either agreement holds, or the faulty processes indeed demonstrate amnesia. 445 \* This property is violated. A counterexample should demonstrate equivocation. 446 AgreementOrAmnesia == 447 Agreement \/ (\A p \in Faulty: AmnesiaBy(p)) 448 449 \* We expect this property to be violated. It shows us a protocol run, 450 \* where one faulty process demonstrates amnesia without equivocation. 451 \* However, the absence of amnesia 452 \* is a tough constraint for Apalache. It has not reported a counterexample 453 \* for n=4,f=2, length <= 5. 454 ShowMeAmnesiaWithoutEquivocation == 455 (~Agreement /\ \E p \in Faulty: ~EquivocationBy(p)) 456 => \A p \in Faulty: ~AmnesiaBy(p) 457 458 \* This property is violated on n=4,f=2, length=4 in less than 10 min. 459 \* Two faulty processes may demonstrate amnesia without equivocation. 460 AmnesiaImpliesEquivocation == 461 (\E p \in Faulty: AmnesiaBy(p)) => (\E q \in Faulty: EquivocationBy(q)) 462 463 (* 464 This property is violated. You can check it to see that all correct processes 465 may reach MaxRound without making a decision. 466 *) 467 NeverUndecidedInMaxRound == 468 LET AllInMax == \A p \in Corr: round[p] = MaxRound 469 AllDecided == \A p \in Corr: decision[p] /= NilValue 470 IN 471 AllInMax => AllDecided 472 473 ============================================================================= 474