github.com/aakash4dev/cometbft@v0.38.2/spec/light-client/detection/LCDetector_003_draft.tla (about)

     1  -------------------------- MODULE LCDetector_003_draft -----------------------------
     2  (**
     3   * This is a specification of the light client detector module.
     4   * It follows the English specification:
     5   *
     6   * https://github.com/aakash4dev/cometbft/blob/main/spec/light-client/detection/detection_003_reviewed.md
     7   *
     8   * The assumptions made in this specification:
     9   *
    10   *  - light client connects to one primary and one secondary peer
    11   *
    12   *  - the light client has its own local clock that can drift from the reference clock
    13   *    within the envelope [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT].
    14   *    The local clock may increase as well as decrease in the the envelope
    15   *    (similar to clock synchronization).
    16   *
    17   *  - the ratio of the faulty validators is set as the parameter.
    18   *
    19   * Igor Konnov, Josef Widder, 2020
    20   *)
    21  
    22  EXTENDS Integers
    23  
    24  \* the parameters of Light Client
    25  CONSTANTS
    26    AllNodes,
    27      (* a set of all nodes that can act as validators (correct and faulty) *)
    28    TRUSTED_HEIGHT,
    29      (* an index of the block header that the light client trusts by social consensus *)
    30    TARGET_HEIGHT,
    31      (* an index of the block header that the light client tries to verify *)
    32    TRUSTING_PERIOD,
    33      (* the period within which the validators are trusted *)
    34    CLOCK_DRIFT,
    35      (* the assumed precision of the clock *)
    36    REAL_CLOCK_DRIFT,
    37      (* the actual clock drift, which under normal circumstances should not
    38         be larger than CLOCK_DRIFT (otherwise, there will be a bug) *)
    39    FAULTY_RATIO,
    40      (* a pair <<a, b>> that limits that ratio of faulty validator in the blockchain
    41         from above (exclusive). Cosmos security model prescribes 1 / 3. *)
    42    IS_PRIMARY_CORRECT,
    43    IS_SECONDARY_CORRECT
    44  
    45  VARIABLES
    46    blockchain,           (* the reference blockchain *)
    47    localClock,           (* the local clock of the light client *)
    48    refClock,             (* the reference clock in the reference blockchain *)
    49    Faulty,               (* the set of faulty validators *)
    50    state,                (* the state of the light client detector *)
    51    fetchedLightBlocks1,  (* a function from heights to LightBlocks *)
    52    fetchedLightBlocks2,  (* a function from heights to LightBlocks *)
    53    fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
    54    commonHeight,         (* the height that is trusted in CreateEvidenceForPeer *)
    55    nextHeightToTry,      (* the index in CreateEvidenceForPeer *)
    56    evidences             (* a set of evidences *)
    57  
    58  vars == <<state, blockchain, localClock, refClock, Faulty,
    59            fetchedLightBlocks1, fetchedLightBlocks2, fetchedLightBlocks1b,
    60            commonHeight, nextHeightToTry, evidences >>
    61  
    62  \* (old) type annotations in Apalache
    63  a <: b == a
    64  
    65   
    66  \* instantiate a reference chain
    67  ULTIMATE_HEIGHT == TARGET_HEIGHT + 1 
    68  BC == INSTANCE Blockchain_003_draft
    69      WITH ULTIMATE_HEIGHT <- (TARGET_HEIGHT + 1)
    70  
    71  \* use the light client API
    72  LC == INSTANCE LCVerificationApi_003_draft
    73  
    74  \* evidence type
    75  ET == [peer |-> STRING, conflictingBlock |-> BC!LBT, commonHeight |-> Int]
    76  
    77  \* is the algorithm in the terminating state
    78  IsTerminated ==
    79      state \in { <<"NoEvidence", "PRIMARY">>,
    80                  <<"NoEvidence", "SECONDARY">>,
    81                  <<"FaultyPeer", "PRIMARY">>,
    82                  <<"FaultyPeer", "SECONDARY">>,
    83                  <<"FoundEvidence", "PRIMARY">> }
    84  
    85  
    86  (********************************* Initialization ******************************)
    87  
    88  \* initialization for the light blocks data structure
    89  InitLightBlocks(lb, Heights) ==
    90      \* BC!LightBlocks is an infinite set, as time is not restricted.
    91      \* Hence, we initialize the light blocks by picking the sets inside.
    92      \E vs, nextVS, lastCommit, commit \in [Heights -> SUBSET AllNodes]:
    93        \* although [Heights -> Int] is an infinite set,
    94        \* Apalache needs just one instance of this set, so it does not complain.
    95        \E timestamp \in [Heights -> Int]:
    96          LET hdr(h) ==
    97               [height |-> h,
    98                time |-> timestamp[h],
    99                VS |-> vs[h],
   100                NextVS |-> nextVS[h],
   101                lastCommit |-> lastCommit[h]]
   102          IN
   103          LET lightHdr(h) ==
   104              [header |-> hdr(h), Commits |-> commit[h]]
   105          IN
   106          lb = [ h \in Heights |-> lightHdr(h) ]
   107  
   108  \* initialize the detector algorithm
   109  Init ==
   110      \* initialize the blockchain to TARGET_HEIGHT + 1
   111      /\ BC!InitToHeight(FAULTY_RATIO)
   112      /\ \E tm \in Int:
   113          tm >= 0 /\ LC!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm
   114      \* start with the secondary looking for evidence
   115      /\ state = <<"Init", "SECONDARY">> /\ commonHeight = 0 /\ nextHeightToTry = 0
   116      /\ evidences = {} <: {ET}
   117      \* Precompute a possible result of light client verification for the primary.
   118      \* It is the input to the detection algorithm.
   119      /\ \E Heights1 \in SUBSET(TRUSTED_HEIGHT..TARGET_HEIGHT):
   120          /\ TRUSTED_HEIGHT \in Heights1
   121          /\ TARGET_HEIGHT \in Heights1
   122          /\ InitLightBlocks(fetchedLightBlocks1, Heights1)
   123          \* As we have a non-deterministic scheduler, for every trace that has
   124          \* an unverified block, there is a filtered trace that only has verified
   125          \* blocks. This is a deep observation.
   126          /\ LET status == [h \in Heights1 |-> "StateVerified"] IN
   127             LC!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT,
   128                                   fetchedLightBlocks1, status,
   129                                   TRUSTED_HEIGHT, TARGET_HEIGHT, "finishedSuccess")
   130      \* initialize the other data structures to the default values
   131      /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT]
   132             trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes]
   133         IN
   134         /\ fetchedLightBlocks2 =  [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
   135         /\ fetchedLightBlocks1b = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
   136  
   137  
   138  (********************************* Transitions ******************************)
   139  
   140  \* a block should contain a copy of the block from the reference chain,
   141  \* with a matching commit
   142  CopyLightBlockFromChain(block, height) ==
   143      LET ref == blockchain[height]
   144          lastCommit ==
   145            IF height < ULTIMATE_HEIGHT
   146            THEN blockchain[height + 1].lastCommit
   147              \* for the ultimate block, which we never use,
   148              \* as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1
   149            ELSE blockchain[height].VS 
   150      IN
   151      block = [header |-> ref, Commits |-> lastCommit]      
   152  
   153  \* Either the primary is correct and the block comes from the reference chain,
   154  \* or the block is produced by a faulty primary.
   155  \*
   156  \* [LCV-FUNC-FETCH.1::TLA.1]
   157  FetchLightBlockInto(isPeerCorrect, block, height) ==
   158      IF isPeerCorrect
   159      THEN CopyLightBlockFromChain(block, height)
   160      ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block)
   161  
   162  
   163  (**
   164   * Pick the next height, for which there is a block.
   165   *)
   166  PickNextHeight(fetchedBlocks, height) ==
   167      LET largerHeights == { h \in DOMAIN fetchedBlocks: h > height } IN
   168      IF largerHeights = ({} <: {Int})
   169      THEN -1
   170      ELSE CHOOSE h \in largerHeights:
   171              \A h2 \in largerHeights: h <= h2
   172  
   173  
   174  (**
   175   * Check, whether the target header matches at the secondary and primary.
   176   *)
   177  CompareLast ==
   178      /\ state = <<"Init", "SECONDARY">>
   179      \* fetch a block from the secondary:
   180      \* non-deterministically pick a block that matches the constraints
   181      /\ \E latest \in BC!LightBlocks:
   182          \* for the moment, we ignore the possibility of a timeout when fetching a block
   183          /\ FetchLightBlockInto(IS_SECONDARY_CORRECT, latest, TARGET_HEIGHT)
   184          /\  IF latest.header = fetchedLightBlocks1[TARGET_HEIGHT].header
   185              THEN \* if the headers match, CreateEvidence is not called
   186                   /\ state' = <<"NoEvidence", "SECONDARY">>
   187                   \* save the retrieved block for further analysis
   188                   /\ fetchedLightBlocks2' =
   189                          [h \in (DOMAIN fetchedLightBlocks2) \union {TARGET_HEIGHT} |->
   190                              IF h = TARGET_HEIGHT THEN latest ELSE fetchedLightBlocks2[h]]
   191                   /\ UNCHANGED <<commonHeight, nextHeightToTry>>
   192              ELSE \* prepare the parameters for CreateEvidence
   193                   /\ commonHeight' = TRUSTED_HEIGHT
   194                   /\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks1, TRUSTED_HEIGHT)
   195                   /\ state' = IF nextHeightToTry' >= 0
   196                               THEN <<"CreateEvidence", "SECONDARY">>
   197                               ELSE <<"FaultyPeer", "SECONDARY">>
   198                   /\ UNCHANGED fetchedLightBlocks2
   199  
   200      /\ UNCHANGED <<blockchain, Faulty,
   201                     fetchedLightBlocks1, fetchedLightBlocks1b, evidences>>
   202  
   203  
   204  \* the actual loop in CreateEvidence
   205  CreateEvidence(peer, isPeerCorrect, refBlocks, targetBlocks) ==
   206      /\ state = <<"CreateEvidence", peer>>
   207      \* precompute a possible result of light client verification for the secondary
   208      \* we have to introduce HeightRange, because Apalache can only handle a..b
   209      \* for constant a and b
   210      /\ LET HeightRange == { h \in TRUSTED_HEIGHT..TARGET_HEIGHT:
   211                                  commonHeight <= h /\ h <= nextHeightToTry } IN
   212        \E HeightsRange \in SUBSET(HeightRange):
   213          /\ commonHeight \in HeightsRange /\ nextHeightToTry \in HeightsRange
   214          /\ InitLightBlocks(targetBlocks, HeightsRange)
   215          \* As we have a non-deterministic scheduler, for every trace that has
   216          \* an unverified block, there is a filtered trace that only has verified
   217          \* blocks. This is a deep observation.
   218          /\ \E result \in {"finishedSuccess", "finishedFailure"}:
   219              LET targetStatus == [h \in HeightsRange |-> "StateVerified"] IN
   220              \* call VerifyToTarget for (commonHeight, nextHeightToTry).
   221              /\ LC!VerifyToTargetPost(blockchain, isPeerCorrect,
   222                                       targetBlocks, targetStatus,
   223                                       commonHeight, nextHeightToTry, result)
   224                 \* case 1: the peer has failed (or the trusting period has expired)
   225              /\ \/ /\ result /= "finishedSuccess"
   226                    /\ state' = <<"FaultyPeer", peer>>
   227                    /\ UNCHANGED <<commonHeight, nextHeightToTry, evidences>>
   228                 \* case 2: success
   229                 \/ /\ result = "finishedSuccess"
   230                    /\ LET block1 == refBlocks[nextHeightToTry] IN
   231                       LET block2 == targetBlocks[nextHeightToTry] IN
   232                       IF block1.header /= block2.header
   233                       THEN \* the target blocks do not match
   234                         /\ state' = <<"FoundEvidence", peer>>
   235                         /\ evidences' = evidences \union
   236                              {[peer |-> peer,
   237                                conflictingBlock |-> block1,
   238                                commonHeight |-> commonHeight]}
   239                         /\ UNCHANGED <<commonHeight, nextHeightToTry>>
   240                       ELSE \* the target blocks match
   241                         /\ nextHeightToTry' = PickNextHeight(refBlocks, nextHeightToTry)
   242                         /\ commonHeight' = nextHeightToTry
   243                         /\ state' = IF nextHeightToTry' >= 0
   244                                     THEN state
   245                                     ELSE <<"NoEvidence", peer>>
   246                         /\ UNCHANGED evidences  
   247  
   248  SwitchToPrimary ==
   249      /\ state = <<"FoundEvidence", "SECONDARY">>
   250      /\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks2, commonHeight)
   251      /\ state' = <<"CreateEvidence", "PRIMARY">>
   252      /\ UNCHANGED <<blockchain, refClock, Faulty, localClock,
   253                     fetchedLightBlocks1, fetchedLightBlocks2, fetchedLightBlocks1b,
   254                     commonHeight, evidences >>
   255  
   256  
   257  CreateEvidenceForSecondary ==
   258    /\ CreateEvidence("SECONDARY", IS_SECONDARY_CORRECT,
   259                      fetchedLightBlocks1, fetchedLightBlocks2')
   260    /\ UNCHANGED <<blockchain, refClock, Faulty, localClock,
   261          fetchedLightBlocks1, fetchedLightBlocks1b>>
   262  
   263  CreateEvidenceForPrimary ==
   264    /\ CreateEvidence("PRIMARY", IS_PRIMARY_CORRECT,
   265                      fetchedLightBlocks2,
   266                      fetchedLightBlocks1b')
   267    /\ UNCHANGED <<blockchain, Faulty,
   268          fetchedLightBlocks1, fetchedLightBlocks2>>
   269  
   270  (*
   271    The local and global clocks can be updated. They can also drift from each other.
   272    Note that the local clock can actually go backwards in time.
   273    However, it still stays in the drift envelope
   274    of [refClock - REAL_CLOCK_DRIFT, refClock + REAL_CLOCK_DRIFT].
   275   *)
   276  AdvanceClocks ==
   277      /\ \E tm \in Int:
   278          tm >= refClock /\ refClock' = tm
   279      /\ \E tm \in Int:
   280          /\ tm >= localClock
   281          /\ LC!IsLocalClockWithinDrift(tm, refClock')
   282          /\ localClock' = tm
   283     
   284  (**
   285   Execute AttackDetector for one secondary.
   286  
   287   [LCD-FUNC-DETECTOR.2::LOOP.1]
   288   *)
   289  Next ==
   290      /\ AdvanceClocks
   291      /\ \/ CompareLast
   292         \/ CreateEvidenceForSecondary
   293         \/ SwitchToPrimary 
   294         \/ CreateEvidenceForPrimary
   295  
   296  
   297  \* simple invariants to see the progress of the detector
   298  NeverNoEvidence == state[1] /= "NoEvidence"
   299  NeverFoundEvidence == state[1] /= "FoundEvidence"
   300  NeverFaultyPeer == state[1] /= "FaultyPeer"
   301  NeverCreateEvidence == state[1] /= "CreateEvidence"
   302  
   303  NeverFoundEvidencePrimary == state /= <<"FoundEvidence", "PRIMARY">>
   304  
   305  NeverReachTargetHeight == nextHeightToTry < TARGET_HEIGHT
   306  
   307  EvidenceWhenFaultyInv ==
   308      (state[1] = "FoundEvidence") => (~IS_PRIMARY_CORRECT \/ ~IS_SECONDARY_CORRECT)
   309  
   310  NoEvidenceForCorrectInv ==
   311      IS_PRIMARY_CORRECT /\ IS_SECONDARY_CORRECT => evidences = {} <: {ET}
   312  
   313  (**
   314   * If we find an evidence by peer A, peer B has ineded given us a corrupted
   315   * header following the common height. Also, we have a verification trace by peer A.
   316   *)
   317  CommonHeightOnEvidenceInv ==
   318    \A e \in evidences:
   319      LET conflicting == e.conflictingBlock IN
   320      LET conflictingHeader == conflicting.header IN
   321      \* the evidence by suspectingPeer can be verified by suspectingPeer in one step
   322      LET SoundEvidence(suspectingPeer, peerBlocks) ==
   323        \/ e.peer /= suspectingPeer
   324          \* the conflicting block from another peer verifies against the common height
   325        \/ /\ "SUCCESS" =
   326              LC!ValidAndVerifiedUntimed(peerBlocks[e.commonHeight], conflicting)
   327           \* and the headers of the same height by the two peers do not match
   328           /\ peerBlocks[conflictingHeader.height].header /= conflictingHeader
   329      IN
   330      /\ SoundEvidence("PRIMARY", fetchedLightBlocks1b)
   331      /\ SoundEvidence("SECONDARY", fetchedLightBlocks2)
   332  
   333  (**
   334   * If the light client does not find an evidence,
   335   * then there is no attack on the light client.
   336   *)
   337  AccuracyInv ==
   338    (LC!InTrustingPeriodLocal(fetchedLightBlocks1[TARGET_HEIGHT].header)
   339          /\ state = <<"NoEvidence", "SECONDARY">>)
   340          =>
   341      (fetchedLightBlocks1[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT]
   342        /\ fetchedLightBlocks2[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT])
   343  
   344  (**
   345   * The primary reports a corrupted block at the target height. If the secondary is
   346   * correct and the algorithm has terminated, we should get the evidence.
   347   * This property is violated due to clock drift. VerifyToTarget may fail with
   348   * the correct secondary within the trusting period (due to clock drift, locally
   349   * we think that we are outside of the trusting period).
   350   *)
   351  PrecisionInvGrayZone ==
   352      (/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT]
   353       /\ BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
   354       /\ IS_SECONDARY_CORRECT
   355       /\ IsTerminated)
   356         =>
   357      evidences /= {} <: {ET}
   358  
   359  (**
   360   * The primary reports a corrupted block at the target height. If the secondary is
   361   * correct and the algorithm has terminated, we should get the evidence.
   362   * This invariant does not fail, as we are using the local clock to check the trusting
   363   * period.
   364   *)
   365  PrecisionInvLocal ==
   366      (/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT]
   367       /\ LC!InTrustingPeriodLocalSurely(blockchain[TRUSTED_HEIGHT])
   368       /\ IS_SECONDARY_CORRECT
   369       /\ IsTerminated)
   370         =>
   371      evidences /= {} <: {ET}
   372  
   373  ====================================================================================