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