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