github.com/Team-Kujira/tendermint@v0.34.24-indexer/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