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