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