github.com/vipernet-xyz/tm@v0.34.24/spec/light-client/verification/Lightclient_003_draft.tla (about)

     1  -------------------------- MODULE Lightclient_003_draft ----------------------------
     2  (**
     3   * A state-machine specification of the lite client verification,
     4   * following the English spec:
     5   *
     6   * https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification.md
     7   *) 
     8  
     9  EXTENDS Integers, FiniteSets
    10  
    11  \* the parameters of Light Client
    12  CONSTANTS
    13    TRUSTED_HEIGHT,
    14      (* an index of the block header that the light client trusts by social consensus *)
    15    TARGET_HEIGHT,
    16      (* an index of the block header that the light client tries to verify *)
    17    TRUSTING_PERIOD,
    18      (* the period within which the validators are trusted *)
    19    CLOCK_DRIFT,
    20      (* the assumed precision of the clock *)
    21    REAL_CLOCK_DRIFT,
    22      (* the actual clock drift, which under normal circumstances should not
    23         be larger than CLOCK_DRIFT (otherwise, there will be a bug) *)
    24    IS_PRIMARY_CORRECT,
    25      (* is primary correct? *)  
    26    FAULTY_RATIO
    27      (* a pair <<a, b>> that limits that ratio of faulty validator in the blockchain
    28         from above (exclusive). Tendermint security model prescribes 1 / 3. *)
    29  
    30  VARIABLES       (* see TypeOK below for the variable types *)
    31    localClock,   (* the local clock of the light client *)
    32    state,        (* the current state of the light client *)
    33    nextHeight,   (* the next height to explore by the light client *)
    34    nprobes       (* the lite client iteration, or the number of block tests *)
    35    
    36  (* the light store *)
    37  VARIABLES  
    38    fetchedLightBlocks, (* a function from heights to LightBlocks *)
    39    lightBlockStatus,   (* a function from heights to block statuses *)
    40    latestVerified      (* the latest verified block *)
    41  
    42  (* the variables of the lite client *)
    43  lcvars == <<localClock, state, nextHeight,
    44              fetchedLightBlocks, lightBlockStatus, latestVerified>>
    45  
    46  (* the light client previous state components, used for monitoring *)
    47  VARIABLES
    48    prevVerified,
    49    prevCurrent,
    50    prevLocalClock,
    51    prevVerdict
    52  
    53  InitMonitor(verified, current, pLocalClock, verdict) ==
    54    /\ prevVerified = verified
    55    /\ prevCurrent = current
    56    /\ prevLocalClock = pLocalClock
    57    /\ prevVerdict = verdict
    58  
    59  NextMonitor(verified, current, pLocalClock, verdict) ==
    60    /\ prevVerified' = verified
    61    /\ prevCurrent' = current
    62    /\ prevLocalClock' = pLocalClock
    63    /\ prevVerdict' = verdict
    64  
    65  
    66  (******************* Blockchain instance ***********************************)
    67  
    68  \* the parameters that are propagated into Blockchain
    69  CONSTANTS
    70    AllNodes
    71      (* a set of all nodes that can act as validators (correct and faulty) *)
    72  
    73  \* the state variables of Blockchain, see Blockchain.tla for the details
    74  VARIABLES refClock, blockchain, Faulty
    75  
    76  \* All the variables of Blockchain. For some reason, BC!vars does not work
    77  bcvars == <<refClock, blockchain, Faulty>>
    78  
    79  (* Create an instance of Blockchain.
    80     We could write EXTENDS Blockchain, but then all the constants and state variables
    81     would be hidden inside the Blockchain module.
    82   *) 
    83  ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 
    84   
    85  BC == INSTANCE Blockchain_003_draft WITH
    86    refClock <- refClock, blockchain <- blockchain, Faulty <- Faulty
    87  
    88  (************************** Lite client ************************************)
    89  
    90  (* the heights on which the light client is working *)  
    91  HEIGHTS == TRUSTED_HEIGHT..TARGET_HEIGHT
    92  
    93  (* the control states of the lite client *) 
    94  States == { "working", "finishedSuccess", "finishedFailure" }
    95  
    96  \* The verification functions are implemented in the API
    97  API == INSTANCE LCVerificationApi_003_draft
    98  
    99  
   100  (*
   101   Initial states of the light client.
   102   Initially, only the trusted light block is present.
   103   *)
   104  LCInit ==
   105      /\ \E tm \in Int:
   106          tm >= 0 /\ API!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm
   107      /\ state = "working"
   108      /\ nextHeight = TARGET_HEIGHT
   109      /\ nprobes = 0  \* no tests have been done so far
   110      /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT]
   111             trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes]
   112         IN
   113          \* initially, fetchedLightBlocks is a function of one element, i.e., TRUSTED_HEIGHT
   114          /\ fetchedLightBlocks = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
   115          \* initially, lightBlockStatus is a function of one element, i.e., TRUSTED_HEIGHT
   116          /\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"]
   117          \* the latest verified block the the trusted block
   118          /\ latestVerified = trustedLightBlock
   119          /\ InitMonitor(trustedLightBlock, trustedLightBlock, localClock, "SUCCESS")
   120  
   121  \* block should contain a copy of the block from the reference chain, with a matching commit
   122  CopyLightBlockFromChain(block, height) ==
   123      LET ref == blockchain[height]
   124          lastCommit ==
   125            IF height < ULTIMATE_HEIGHT
   126            THEN blockchain[height + 1].lastCommit
   127              \* for the ultimate block, which we never use, as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1
   128            ELSE blockchain[height].VS 
   129      IN
   130      block = [header |-> ref, Commits |-> lastCommit]      
   131  
   132  \* Either the primary is correct and the block comes from the reference chain,
   133  \* or the block is produced by a faulty primary.
   134  \*
   135  \* [LCV-FUNC-FETCH.1::TLA.1]
   136  FetchLightBlockInto(block, height) ==
   137      IF IS_PRIMARY_CORRECT
   138      THEN CopyLightBlockFromChain(block, height)
   139      ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block)
   140  
   141  \* add a block into the light store    
   142  \*
   143  \* [LCV-FUNC-UPDATE.1::TLA.1]
   144  LightStoreUpdateBlocks(lightBlocks, block) ==
   145      LET ht == block.header.height IN    
   146      [h \in DOMAIN lightBlocks \union {ht} |->
   147          IF h = ht THEN block ELSE lightBlocks[h]]
   148  
   149  \* update the state of a light block      
   150  \*
   151  \* [LCV-FUNC-UPDATE.1::TLA.1]
   152  LightStoreUpdateStates(statuses, ht, blockState) ==
   153      [h \in DOMAIN statuses \union {ht} |->
   154          IF h = ht THEN blockState ELSE statuses[h]]      
   155  
   156  \* Check, whether newHeight is a possible next height for the light client.
   157  \*
   158  \* [LCV-FUNC-SCHEDULE.1::TLA.1]
   159  CanScheduleTo(newHeight, pLatestVerified, pNextHeight, pTargetHeight) ==
   160      LET ht == pLatestVerified.header.height IN
   161      \/ /\ ht = pNextHeight
   162         /\ ht < pTargetHeight
   163         /\ pNextHeight < newHeight
   164         /\ newHeight <= pTargetHeight
   165      \/ /\ ht < pNextHeight
   166         /\ ht < pTargetHeight
   167         /\ ht < newHeight
   168         /\ newHeight < pNextHeight
   169      \/ /\ ht = pTargetHeight
   170         /\ newHeight = pTargetHeight
   171  
   172  \* The loop of VerifyToTarget.
   173  \*
   174  \* [LCV-FUNC-MAIN.1::TLA-LOOP.1]
   175  VerifyToTargetLoop ==
   176        \* the loop condition is true
   177      /\ latestVerified.header.height < TARGET_HEIGHT
   178        \* pick a light block, which will be constrained later
   179      /\ \E current \in BC!LightBlocks:
   180          \* Get next LightBlock for verification
   181          /\ IF nextHeight \in DOMAIN fetchedLightBlocks
   182             THEN \* copy the block from the light store
   183                  /\ current = fetchedLightBlocks[nextHeight]
   184                  /\ UNCHANGED fetchedLightBlocks
   185             ELSE \* retrieve a light block and save it in the light store
   186                  /\ FetchLightBlockInto(current, nextHeight)
   187                  /\ fetchedLightBlocks' = LightStoreUpdateBlocks(fetchedLightBlocks, current)
   188          \* Record that one more probe has been done (for complexity and model checking)
   189          /\ nprobes' = nprobes + 1
   190          \* Verify the current block
   191          /\ LET verdict == API!ValidAndVerified(latestVerified, current, TRUE) IN
   192             NextMonitor(latestVerified, current, localClock, verdict) /\
   193             \* Decide whether/how to continue
   194             CASE verdict = "SUCCESS" ->
   195                /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified")
   196                /\ latestVerified' = current
   197                /\ state' =
   198                      IF latestVerified'.header.height < TARGET_HEIGHT
   199                      THEN "working"
   200                      ELSE "finishedSuccess"
   201                /\ \E newHeight \in HEIGHTS:
   202                   /\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT)
   203                   /\ nextHeight' = newHeight
   204                    
   205             [] verdict = "NOT_ENOUGH_TRUST" ->
   206                (*
   207                  do nothing: the light block current passed validation, but the validator
   208                  set is too different to verify it. We keep the state of
   209                  current at StateUnverified. For a later iteration, Schedule
   210                  might decide to try verification of that light block again.
   211                  *)
   212                /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateUnverified")
   213                /\ \E newHeight \in HEIGHTS:
   214                   /\ CanScheduleTo(newHeight, latestVerified, nextHeight, TARGET_HEIGHT)
   215                   /\ nextHeight' = newHeight 
   216                /\ UNCHANGED <<latestVerified, state>>
   217                
   218             [] OTHER ->
   219                \* verdict is some error code
   220                /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateFailed")
   221                /\ state' = "finishedFailure"
   222                /\ UNCHANGED <<latestVerified, nextHeight>>
   223  
   224  \* The terminating condition of VerifyToTarget.
   225  \*
   226  \* [LCV-FUNC-MAIN.1::TLA-LOOPCOND.1]
   227  VerifyToTargetDone ==
   228      /\ latestVerified.header.height >= TARGET_HEIGHT
   229      /\ state' = "finishedSuccess"
   230      /\ UNCHANGED <<nextHeight, nprobes, fetchedLightBlocks, lightBlockStatus, latestVerified>>
   231      /\ UNCHANGED <<prevVerified, prevCurrent, prevLocalClock, prevVerdict>>
   232  
   233  (*
   234    The local and global clocks can be updated. They can also drift from each other.
   235    Note that the local clock can actually go backwards in time.
   236    However, it still stays in the drift envelope
   237    of [refClock - REAL_CLOCK_DRIFT, refClock + REAL_CLOCK_DRIFT].
   238   *)
   239  AdvanceClocks ==
   240      /\ BC!AdvanceTime
   241      /\ \E tm \in Int:
   242          /\ tm >= 0
   243          /\ API!IsLocalClockWithinDrift(tm, refClock')
   244          /\ localClock' = tm
   245          \* if you like the clock to always grow monotonically, uncomment the next line:
   246          \*/\ localClock' > localClock
   247              
   248  (********************* Lite client + Blockchain *******************)
   249  Init ==
   250      \* the blockchain is initialized immediately to the ULTIMATE_HEIGHT
   251      /\ BC!InitToHeight(FAULTY_RATIO)
   252      \* the light client starts
   253      /\ LCInit
   254  
   255  (*
   256    The system step is very simple.
   257    The light client is either executing VerifyToTarget, or it has terminated.
   258    (In the latter case, a model checker reports a deadlock.)
   259    Simultaneously, the global clock may advance.
   260   *)
   261  Next ==
   262      /\ state = "working"
   263      /\ VerifyToTargetLoop \/ VerifyToTargetDone 
   264      /\ AdvanceClocks
   265  
   266  (************************* Types ******************************************)
   267  TypeOK ==
   268      /\ state \in States
   269      /\ localClock \in Nat
   270      /\ refClock \in Nat
   271      /\ nextHeight \in HEIGHTS
   272      /\ latestVerified \in BC!LightBlocks
   273      /\ \E HS \in SUBSET HEIGHTS:
   274          /\ fetchedLightBlocks \in [HS -> BC!LightBlocks]
   275          /\ lightBlockStatus
   276               \in [HS -> {"StateVerified", "StateUnverified", "StateFailed"}]
   277  
   278  (************************* Properties ******************************************)
   279  
   280  (* The properties to check *)
   281  \* this invariant candidate is false    
   282  NeverFinish ==
   283      state = "working"
   284  
   285  \* this invariant candidate is false    
   286  NeverFinishNegative ==
   287      state /= "finishedFailure"
   288  
   289  \* This invariant holds true, when the primary is correct. 
   290  \* This invariant candidate is false when the primary is faulty.    
   291  NeverFinishNegativeWhenTrusted ==
   292      BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
   293        => state /= "finishedFailure"     
   294  
   295  \* this invariant candidate is false    
   296  NeverFinishPositive ==
   297      state /= "finishedSuccess"
   298  
   299  
   300  (**
   301   Check that the target height has been reached upon successful termination.
   302   *)
   303  TargetHeightOnSuccessInv ==
   304      state = "finishedSuccess" =>
   305          /\ TARGET_HEIGHT \in DOMAIN fetchedLightBlocks
   306          /\ lightBlockStatus[TARGET_HEIGHT] = "StateVerified"
   307  
   308  (**
   309    Correctness states that all the obtained headers are exactly like in the blockchain.
   310   
   311    It is always the case that every verified header in LightStore was generated by
   312    an instance of Tendermint consensus.
   313    
   314    [LCV-DIST-SAFE.1::CORRECTNESS-INV.1]
   315   *)  
   316  CorrectnessInv ==
   317      \A h \in DOMAIN fetchedLightBlocks:
   318          lightBlockStatus[h] = "StateVerified" =>
   319              fetchedLightBlocks[h].header = blockchain[h]
   320  
   321  (**
   322   No faulty block was used to construct a proof. This invariant holds,
   323   only if FAULTY_RATIO < 1/3.
   324   *)
   325  NoTrustOnFaultyBlockInv ==
   326      (state = "finishedSuccess"
   327          /\ fetchedLightBlocks[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT])
   328          => CorrectnessInv
   329  
   330  (**
   331   Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "SUCCESS" pairwise
   332   This property is easily violated, whenever a header cannot be trusted anymore.
   333   *)
   334  StoredHeadersAreVerifiedInv ==
   335      state = "finishedSuccess"
   336          =>
   337          \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers
   338              \/ lh >= rh
   339                 \* either there is a header between them
   340              \/ \E mh \in DOMAIN fetchedLightBlocks:
   341                  lh < mh /\ mh < rh
   342                 \* or we can verify the right one using the left one
   343              \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
   344                                                  fetchedLightBlocks[rh], FALSE)
   345  
   346  \* An improved version of StoredHeadersAreVerifiedInv,
   347  \* assuming that a header may be not trusted.
   348  \* This invariant candidate is also violated,
   349  \* as there may be some unverified blocks left in the middle.
   350  \* This property is violated under two conditions:
   351  \* (1) the primary is faulty and there are at least 4 blocks,
   352  \* (2) the primary is correct and there are at least 5 blocks.
   353  StoredHeadersAreVerifiedOrNotTrustedInv ==
   354      state = "finishedSuccess"
   355          =>
   356          \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers
   357              \/ lh >= rh
   358                 \* either there is a header between them
   359              \/ \E mh \in DOMAIN fetchedLightBlocks:
   360                  lh < mh /\ mh < rh
   361                 \* or we can verify the right one using the left one
   362              \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
   363                                                  fetchedLightBlocks[rh], FALSE)
   364                 \* or the left header is outside the trusting period, so no guarantees
   365              \/ ~API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header) 
   366  
   367  (**
   368   * An improved version of StoredHeadersAreSoundOrNotTrusted,
   369   * checking the property only for the verified headers.
   370   * This invariant holds true if CLOCK_DRIFT <= REAL_CLOCK_DRIFT.
   371   *)
   372  ProofOfChainOfTrustInv ==
   373      state = "finishedSuccess"
   374          =>
   375          \A lh, rh \in DOMAIN fetchedLightBlocks:
   376                  \* for every pair of stored headers that have been verified
   377              \/ lh >= rh
   378              \/ lightBlockStatus[lh] = "StateUnverified"
   379              \/ lightBlockStatus[rh] = "StateUnverified"
   380                 \* either there is a header between them
   381              \/ \E mh \in DOMAIN fetchedLightBlocks:
   382                  lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified"
   383                 \* or the left header is outside the trusting period, so no guarantees
   384              \/ ~(API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header))
   385                 \* or we can verify the right one using the left one
   386              \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
   387                                                  fetchedLightBlocks[rh], FALSE)
   388  
   389  (**
   390   * When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.) 
   391   *)            
   392  NoFailedBlocksOnSuccessInv ==
   393      state = "finishedSuccess" =>
   394          \A h \in DOMAIN fetchedLightBlocks:
   395              lightBlockStatus[h] /= "StateFailed"            
   396  
   397  \* This property states that whenever the light client finishes with a positive outcome,
   398  \* the trusted header is still within the trusting period.
   399  \* We expect this property to be violated. And Apalache shows us a counterexample.
   400  PositiveBeforeTrustedHeaderExpires ==
   401      (state = "finishedSuccess") =>
   402          BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
   403      
   404  \* If the primary is correct and the initial trusted block has not expired,
   405  \* then whenever the algorithm terminates, it reports "success".
   406  \* This property fails.
   407  CorrectPrimaryAndTimeliness ==
   408    (BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
   409      /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
   410        state = "finishedSuccess"     
   411  
   412  (**
   413    If the primary is correct and there is a trusted block that has not expired,
   414    then whenever the algorithm terminates, it reports "success".
   415    This property only holds true, if the local clock is always growing monotonically.
   416    If the local clock can go backwards in the envelope
   417    [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT], then the property fails.
   418  
   419    [LCV-DIST-LIVE.1::SUCCESS-CORR-PRIMARY-CHAIN-OF-TRUST.1]
   420   *)
   421  SuccessOnCorrectPrimaryAndChainOfTrustLocal ==
   422    (\E h \in DOMAIN fetchedLightBlocks:
   423          /\ lightBlockStatus[h] = "StateVerified"
   424          /\ API!InTrustingPeriodLocal(blockchain[h])
   425      /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
   426        state = "finishedSuccess"     
   427  
   428  (**
   429    Similar to SuccessOnCorrectPrimaryAndChainOfTrust, but using the blockchain clock.
   430    It fails because the local clock of the client drifted away, so it rejects a block
   431    that has not expired yet (according to the local clock).
   432   *)
   433  SuccessOnCorrectPrimaryAndChainOfTrustGlobal ==
   434    (\E h \in DOMAIN fetchedLightBlocks:
   435          lightBlockStatus[h] = "StateVerified" /\ BC!InTrustingPeriod(blockchain[h])
   436      /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
   437        state = "finishedSuccess"     
   438  
   439  \* Lite Client Completeness: If header h was correctly generated by an instance
   440  \* of Tendermint consensus (and its age is less than the trusting period),
   441  \* then the lite client should eventually set trust(h) to true.
   442  \*
   443  \* Note that Completeness assumes that the lite client communicates with a correct full node.
   444  \*
   445  \* We decompose completeness into Termination (liveness) and Precision (safety).
   446  \* Once again, Precision is an inverse version of the safety property in Completeness,
   447  \* as A => B is logically equivalent to ~B => ~A. 
   448  \*
   449  \* This property holds only when CLOCK_DRIFT = 0 and REAL_CLOCK_DRIFT = 0.
   450  PrecisionInv ==
   451      (state = "finishedFailure")
   452        => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period
   453           \/ \E h \in DOMAIN fetchedLightBlocks:
   454              LET lightBlock == fetchedLightBlocks[h] IN
   455                   \* the full node lied to the lite client about the block header
   456                \/ lightBlock.header /= blockchain[h]
   457                   \* the full node lied to the lite client about the commits
   458                \/ lightBlock.Commits /= lightBlock.header.VS
   459  
   460  \* the old invariant that was found to be buggy by TLC
   461  PrecisionBuggyInv ==
   462      (state = "finishedFailure")
   463        => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period
   464           \/ \E h \in DOMAIN fetchedLightBlocks:
   465              LET lightBlock == fetchedLightBlocks[h] IN
   466              \* the full node lied to the lite client about the block header
   467              lightBlock.header /= blockchain[h]
   468  
   469  \* the worst complexity
   470  Complexity ==
   471      LET N == TARGET_HEIGHT - TRUSTED_HEIGHT + 1 IN
   472      state /= "working" =>
   473          (2 * nprobes <= N * (N - 1)) 
   474  
   475  (**
   476   If the light client has terminated, then the expected postcondition holds true.
   477   *)
   478  ApiPostInv ==
   479      state /= "working" =>
   480          API!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT,
   481                  fetchedLightBlocks, lightBlockStatus,
   482                  TRUSTED_HEIGHT, TARGET_HEIGHT, state)
   483  
   484  (*
   485   We omit termination, as the algorithm deadlocks in the end.
   486   So termination can be demonstrated by finding a deadlock.
   487   Of course, one has to analyze the deadlocked state and see that
   488   the algorithm has indeed terminated there.
   489  *)
   490  =============================================================================
   491  \* Modification History
   492  \* Last modified Fri Jun 26 12:08:28 CEST 2020 by igor
   493  \* Created Wed Oct 02 16:39:42 CEST 2019 by igor