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