github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/spec/light-client/accountability/TendermintAccInv_004_draft.tla (about) 1 ------------------- MODULE TendermintAccInv_004_draft -------------------------- 2 (* 3 An inductive invariant for TendermintAcc3, which capture the forked 4 and non-forked cases. 5 6 * Version 3. Modular and parameterized definitions. 7 * Version 2. Bugfixes in the spec and an inductive invariant. 8 9 Igor Konnov, 2020. 10 *) 11 12 EXTENDS TendermintAcc_004_draft 13 14 (************************** TYPE INVARIANT ***********************************) 15 (* first, we define the sets of all potential messages *) 16 AllProposals == 17 [type: {"PROPOSAL"}, 18 src: AllProcs, 19 round: Rounds, 20 proposal: ValuesOrNil, 21 validRound: RoundsOrNil] 22 23 AllPrevotes == 24 [type: {"PREVOTE"}, 25 src: AllProcs, 26 round: Rounds, 27 id: ValuesOrNil] 28 29 AllPrecommits == 30 [type: {"PRECOMMIT"}, 31 src: AllProcs, 32 round: Rounds, 33 id: ValuesOrNil] 34 35 (* the standard type invariant -- importantly, it is inductive *) 36 TypeOK == 37 /\ round \in [Corr -> Rounds] 38 /\ step \in [Corr -> { "PROPOSE", "PREVOTE", "PRECOMMIT", "DECIDED" }] 39 /\ decision \in [Corr -> ValidValues \union {NilValue}] 40 /\ lockedValue \in [Corr -> ValidValues \union {NilValue}] 41 /\ lockedRound \in [Corr -> RoundsOrNil] 42 /\ validValue \in [Corr -> ValidValues \union {NilValue}] 43 /\ validRound \in [Corr -> RoundsOrNil] 44 /\ msgsPropose \in [Rounds -> SUBSET AllProposals] 45 /\ BenignRoundsInMessages(msgsPropose) 46 /\ msgsPrevote \in [Rounds -> SUBSET AllPrevotes] 47 /\ BenignRoundsInMessages(msgsPrevote) 48 /\ msgsPrecommit \in [Rounds -> SUBSET AllPrecommits] 49 /\ BenignRoundsInMessages(msgsPrecommit) 50 /\ evidencePropose \in SUBSET AllProposals 51 /\ evidencePrevote \in SUBSET AllPrevotes 52 /\ evidencePrecommit \in SUBSET AllPrecommits 53 /\ action \in { 54 "Init", 55 "InsertProposal", 56 "UponProposalInPropose", 57 "UponProposalInProposeAndPrevote", 58 "UponQuorumOfPrevotesAny", 59 "UponProposalInPrevoteOrCommitAndPrevote", 60 "UponQuorumOfPrecommitsAny", 61 "UponProposalInPrecommitNoDecision", 62 "OnTimeoutPropose", 63 "OnQuorumOfNilPrevotes", 64 "OnRoundCatchup" 65 } 66 67 (************************** INDUCTIVE INVARIANT *******************************) 68 EvidenceContainsMessages == 69 \* evidence contains only the messages from: 70 \* msgsPropose, msgsPrevote, and msgsPrecommit 71 /\ \A m \in evidencePropose: 72 m \in msgsPropose[m.round] 73 /\ \A m \in evidencePrevote: 74 m \in msgsPrevote[m.round] 75 /\ \A m \in evidencePrecommit: 76 m \in msgsPrecommit[m.round] 77 78 NoFutureMessagesForLargerRounds(p) == 79 \* a correct process does not send messages for the future rounds 80 \A r \in { rr \in Rounds: rr > round[p] }: 81 /\ \A m \in msgsPropose[r]: m.src /= p 82 /\ \A m \in msgsPrevote[r]: m.src /= p 83 /\ \A m \in msgsPrecommit[r]: m.src /= p 84 85 NoFutureMessagesForCurrentRound(p) == 86 \* a correct process does not send messages in the future 87 LET r == round[p] IN 88 /\ Proposer[r] = p \/ \A m \in msgsPropose[r]: m.src /= p 89 /\ \/ step[p] \in {"PREVOTE", "PRECOMMIT", "DECIDED"} 90 \/ \A m \in msgsPrevote[r]: m.src /= p 91 /\ \/ step[p] \in {"PRECOMMIT", "DECIDED"} 92 \/ \A m \in msgsPrecommit[r]: m.src /= p 93 94 \* the correct processes never send future messages 95 AllNoFutureMessagesSent == 96 \A p \in Corr: 97 /\ NoFutureMessagesForCurrentRound(p) 98 /\ NoFutureMessagesForLargerRounds(p) 99 100 \* a correct process in the PREVOTE state has sent a PREVOTE message 101 IfInPrevoteThenSentPrevote(p) == 102 step[p] = "PREVOTE" => 103 \E m \in msgsPrevote[round[p]]: 104 /\ m.id \in ValidValues \cup { NilValue } 105 /\ m.src = p 106 107 AllIfInPrevoteThenSentPrevote == 108 \A p \in Corr: IfInPrevoteThenSentPrevote(p) 109 110 \* a correct process in the PRECOMMIT state has sent a PRECOMMIT message 111 IfInPrecommitThenSentPrecommit(p) == 112 step[p] = "PRECOMMIT" => 113 \E m \in msgsPrecommit[round[p]]: 114 /\ m.id \in ValidValues \cup { NilValue } 115 /\ m.src = p 116 117 AllIfInPrecommitThenSentPrecommit == 118 \A p \in Corr: IfInPrecommitThenSentPrecommit(p) 119 120 \* a process in the PRECOMMIT state has sent a PRECOMMIT message 121 IfInDecidedThenValidDecision(p) == 122 step[p] = "DECIDED" <=> decision[p] \in ValidValues 123 124 AllIfInDecidedThenValidDecision == 125 \A p \in Corr: IfInDecidedThenValidDecision(p) 126 127 \* a decided process should have received a proposal on its decision 128 IfInDecidedThenReceivedProposal(p) == 129 step[p] = "DECIDED" => 130 \E r \in Rounds: \* r is not necessarily round[p] 131 /\ \E m \in msgsPropose[r] \intersect evidencePropose: 132 /\ m.src = Proposer[r] 133 /\ m.proposal = decision[p] 134 \* not inductive: /\ m.src \in Corr => (m.validRound <= r) 135 136 AllIfInDecidedThenReceivedProposal == 137 \A p \in Corr: 138 IfInDecidedThenReceivedProposal(p) 139 140 \* a decided process has received two-thirds of precommit messages 141 IfInDecidedThenReceivedTwoThirds(p) == 142 step[p] = "DECIDED" => 143 \E r \in Rounds: 144 LET PV == { 145 m \in msgsPrecommit[r] \intersect evidencePrecommit: 146 m.id = decision[p] 147 } 148 IN 149 Cardinality(PV) >= THRESHOLD2 150 151 AllIfInDecidedThenReceivedTwoThirds == 152 \A p \in Corr: 153 IfInDecidedThenReceivedTwoThirds(p) 154 155 \* for a round r, there is proposal by the round proposer for a valid round vr 156 ProposalInRound(r, proposedVal, vr) == 157 \E m \in msgsPropose[r]: 158 /\ m.src = Proposer[r] 159 /\ m.proposal = proposedVal 160 /\ m.validRound = vr 161 162 TwoThirdsPrevotes(vr, v) == 163 LET PV == { mm \in msgsPrevote[vr] \intersect evidencePrevote: mm.id = v } IN 164 Cardinality(PV) >= THRESHOLD2 165 166 \* if a process sends a PREVOTE, then there are three possibilities: 167 \* 1) the process is faulty, 2) the PREVOTE cotains Nil, 168 \* 3) there is a proposal in an earlier (valid) round and two thirds of PREVOTES 169 IfSentPrevoteThenReceivedProposalOrTwoThirds(r) == 170 \A mpv \in msgsPrevote[r]: 171 \/ mpv.src \in Faulty 172 \* lockedRound and lockedValue is beyond my comprehension 173 \/ mpv.id = NilValue 174 \//\ mpv.src \in Corr 175 /\ mpv.id /= NilValue 176 /\ \/ ProposalInRound(r, mpv.id, NilRound) 177 \/ \E vr \in { rr \in Rounds: rr < r }: 178 /\ ProposalInRound(r, mpv.id, vr) 179 /\ TwoThirdsPrevotes(vr, mpv.id) 180 181 AllIfSentPrevoteThenReceivedProposalOrTwoThirds == 182 \A r \in Rounds: 183 IfSentPrevoteThenReceivedProposalOrTwoThirds(r) 184 185 \* if a correct process has sent a PRECOMMIT, then there are two thirds, 186 \* either on a valid value, or a nil value 187 IfSentPrecommitThenReceivedTwoThirds == 188 \A r \in Rounds: 189 \A mpc \in msgsPrecommit[r]: 190 mpc.src \in Corr => 191 \/ /\ mpc.id \in ValidValues 192 /\ LET PV == { 193 m \in msgsPrevote[r] \intersect evidencePrevote: m.id = mpc.id 194 } 195 IN 196 Cardinality(PV) >= THRESHOLD2 197 \/ /\ mpc.id = NilValue 198 /\ Cardinality(msgsPrevote[r]) >= THRESHOLD2 199 200 \* if a correct process has sent a precommit message in a round, it should 201 \* have sent a prevote 202 IfSentPrecommitThenSentPrevote == 203 \A r \in Rounds: 204 \A mpc \in msgsPrecommit[r]: 205 mpc.src \in Corr => 206 \E m \in msgsPrevote[r]: 207 m.src = mpc.src 208 209 \* there is a locked round if a only if there is a locked value 210 LockedRoundIffLockedValue(p) == 211 (lockedRound[p] = NilRound) <=> (lockedValue[p] = NilValue) 212 213 AllLockedRoundIffLockedValue == 214 \A p \in Corr: 215 LockedRoundIffLockedValue(p) 216 217 \* when a process locked a round, it must have sent a precommit on the locked value. 218 IfLockedRoundThenSentCommit(p) == 219 lockedRound[p] /= NilRound 220 => \E r \in { rr \in Rounds: rr <= round[p] }: 221 \E m \in msgsPrecommit[r]: 222 m.src = p /\ m.id = lockedValue[p] 223 224 AllIfLockedRoundThenSentCommit == 225 \A p \in Corr: 226 IfLockedRoundThenSentCommit(p) 227 228 \* a process always locks the latest round, for which it has sent a PRECOMMIT 229 LatestPrecommitHasLockedRound(p) == 230 LET pPrecommits == 231 {mm \in UNION { msgsPrecommit[r]: r \in Rounds }: mm.src = p /\ mm.id /= NilValue } 232 IN 233 pPrecommits /= {} 234 => LET latest == 235 CHOOSE m \in pPrecommits: 236 \A m2 \in pPrecommits: 237 m2.round <= m.round 238 IN 239 /\ lockedRound[p] = latest.round 240 /\ lockedValue[p] = latest.id 241 242 AllLatestPrecommitHasLockedRound == 243 \A p \in Corr: 244 LatestPrecommitHasLockedRound(p) 245 246 \* Every correct process sends only one value or NilValue. 247 \* This test has quantifier alternation -- a threat to all decision procedures. 248 \* Luckily, the sets Corr and ValidValues are small. 249 \* @type: ($round, $round -> Set($preMsg)) => Bool; 250 NoEquivocationByCorrect(r, msgs) == 251 \A p \in Corr: 252 \E v \in ValidValues \union {NilValue}: 253 \A m \in msgs[r]: 254 \/ m.src /= p 255 \/ m.id = v 256 257 \* a proposer nevers sends two values 258 \* @type: ($round, $round -> Set($proposeMsg)) => Bool; 259 ProposalsByProposer(r, msgs) == 260 \* if the proposer is not faulty, it sends only one value 261 \E v \in ValidValues: 262 \A m \in msgs[r]: 263 \/ m.src \in Faulty 264 \/ m.src = Proposer[r] /\ m.proposal = v 265 266 AllNoEquivocationByCorrect == 267 \A r \in Rounds: 268 /\ ProposalsByProposer(r, msgsPropose) 269 /\ NoEquivocationByCorrect(r, msgsPrevote) 270 /\ NoEquivocationByCorrect(r, msgsPrecommit) 271 272 \* construct the set of the message senders 273 \* @type: (Set({ src: $process, a })) => Set($process); 274 Senders(M) == { m.src: m \in M } 275 276 \* The final piece by Josef Widder: 277 \* if T + 1 processes precommit on the same value in a round, 278 \* then in the future rounds there are less than 2T + 1 prevotes for another value 279 PrecommitsLockValue == 280 \A r \in Rounds: 281 \A v \in ValidValues \union {NilValue}: 282 \/ LET Precommits == {m \in msgsPrecommit[r]: m.id = v} 283 IN 284 Cardinality(Senders(Precommits)) < THRESHOLD1 285 \/ \A fr \in { rr \in Rounds: rr > r }: \* future rounds 286 \A w \in (ValuesOrNil) \ {v}: 287 LET Prevotes == {m \in msgsPrevote[fr]: m.id = w} 288 IN 289 Cardinality(Senders(Prevotes)) < THRESHOLD2 290 291 \* a combination of all lemmas 292 Inv == 293 /\ EvidenceContainsMessages 294 /\ AllNoFutureMessagesSent 295 /\ AllIfInPrevoteThenSentPrevote 296 /\ AllIfInPrecommitThenSentPrecommit 297 /\ AllIfInDecidedThenReceivedProposal 298 /\ AllIfInDecidedThenReceivedTwoThirds 299 /\ AllIfInDecidedThenValidDecision 300 /\ AllLockedRoundIffLockedValue 301 /\ AllIfLockedRoundThenSentCommit 302 /\ AllLatestPrecommitHasLockedRound 303 /\ AllIfSentPrevoteThenReceivedProposalOrTwoThirds 304 /\ IfSentPrecommitThenSentPrevote 305 /\ IfSentPrecommitThenReceivedTwoThirds 306 /\ AllNoEquivocationByCorrect 307 /\ PrecommitsLockValue 308 309 \* this is the inductive invariant we like to check 310 TypedInv == TypeOK /\ Inv 311 312 \* UNUSED FOR SAFETY 313 ValidRoundNotSmallerThanLockedRound(p) == 314 validRound[p] >= lockedRound[p] 315 316 \* UNUSED FOR SAFETY 317 ValidRoundIffValidValue(p) == 318 (validRound[p] = NilRound) <=> (validValue[p] = NilValue) 319 320 \* UNUSED FOR SAFETY 321 AllValidRoundIffValidValue == 322 \A p \in Corr: ValidRoundIffValidValue(p) 323 324 \* if validRound is defined, then there are two-thirds of PREVOTEs 325 IfValidRoundThenTwoThirds(p) == 326 \/ validRound[p] = NilRound 327 \/ LET PV == { m \in msgsPrevote[validRound[p]]: m.id = validValue[p] } IN 328 Cardinality(PV) >= THRESHOLD2 329 330 \* UNUSED FOR SAFETY 331 AllIfValidRoundThenTwoThirds == 332 \A p \in Corr: IfValidRoundThenTwoThirds(p) 333 334 \* a valid round can be only set to a valid value that was proposed earlier 335 IfValidRoundThenProposal(p) == 336 \/ validRound[p] = NilRound 337 \/ \E m \in msgsPropose[validRound[p]]: 338 m.proposal = validValue[p] 339 340 \* UNUSED FOR SAFETY 341 AllIfValidRoundThenProposal == 342 \A p \in Corr: IfValidRoundThenProposal(p) 343 344 (******************************** THEOREMS ***************************************) 345 (* Under this condition, the faulty processes can decide alone *) 346 FaultyQuorum == Cardinality(Faulty) >= THRESHOLD2 347 348 (* The standard condition of the Cosmos security model *) 349 LessThanThirdFaulty == N > 3 * T /\ Cardinality(Faulty) <= T 350 351 (* 352 TypedInv is an inductive invariant, provided that there is no faulty quorum. 353 We run Apalache to prove this theorem only for fixed instances of 4 to 10 processes. 354 (We run Apalache manually, as it does not parse theorem statements at the moment.) 355 To get a parameterized argument, one has to use a theorem prover, e.g., TLAPS. 356 *) 357 THEOREM TypedInvIsInductive == 358 \/ FaultyQuorum \* if there are 2 * T + 1 faulty processes, we give up 359 \//\ Init => TypedInv 360 /\ TypedInv /\ [Next]_vars => TypedInv' 361 362 (* 363 There should be no fork, when there are less than 1/3 faulty processes. 364 *) 365 THEOREM AgreementWhenLessThanThirdFaulty == 366 LessThanThirdFaulty /\ TypedInv => Agreement 367 368 (* 369 In a more general case, when there are less than 2/3 faulty processes, 370 there is either Agreement (no fork), or two scenarios exist: 371 equivocation by Faulty, or amnesia by Faulty. 372 *) 373 THEOREM AgreementOrFork == 374 ~FaultyQuorum /\ TypedInv => Accountability 375 376 ============================================================================= 377