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 ====================================================================================