- -------------------------- MODULE Lightclient_003_draft ----------------------------
- (**
- * A state-machine specification of the lite client verification,
- * following the English spec:
- *
- * https://github.com/informalsystems/tendermint-rs/blob/master/docs/spec/lightclient/verification.md
- *)
-
- EXTENDS Integers, FiniteSets
-
- \* the parameters of Light Client
- CONSTANTS
- TRUSTED_HEIGHT,
- (* an index of the block header that the light client trusts by social consensus *)
- TARGET_HEIGHT,
- (* an index of the block header that the light client tries to verify *)
- TRUSTING_PERIOD,
- (* the period within which the validators are trusted *)
- CLOCK_DRIFT,
- (* the assumed precision of the clock *)
- REAL_CLOCK_DRIFT,
- (* the actual clock drift, which under normal circumstances should not
- be larger than CLOCK_DRIFT (otherwise, there will be a bug) *)
- IS_PRIMARY_CORRECT,
- (* is primary correct? *)
- FAULTY_RATIO
- (* a pair <<a, b>> that limits that ratio of faulty validator in the blockchain
- from above (exclusive). Tendermint security model prescribes 1 / 3. *)
-
- VARIABLES (* see TypeOK below for the variable types *)
- localClock, (* the local clock of the light client *)
- state, (* the current state of the light client *)
- nextHeight, (* the next height to explore by the light client *)
- nprobes (* the lite client iteration, or the number of block tests *)
-
- (* the light store *)
- VARIABLES
- fetchedLightBlocks, (* a function from heights to LightBlocks *)
- lightBlockStatus, (* a function from heights to block statuses *)
- latestVerified (* the latest verified block *)
-
- (* the variables of the lite client *)
- lcvars == <<localClock, state, nextHeight,
- fetchedLightBlocks, lightBlockStatus, latestVerified>>
-
- (* the light client previous state components, used for monitoring *)
- VARIABLES
- prevVerified,
- prevCurrent,
- prevLocalClock,
- prevVerdict
-
- InitMonitor(verified, current, pLocalClock, verdict) ==
- /\ prevVerified = verified
- /\ prevCurrent = current
- /\ prevLocalClock = pLocalClock
- /\ prevVerdict = verdict
-
- NextMonitor(verified, current, pLocalClock, verdict) ==
- /\ prevVerified' = verified
- /\ prevCurrent' = current
- /\ prevLocalClock' = pLocalClock
- /\ prevVerdict' = verdict
-
-
- (******************* Blockchain instance ***********************************)
-
- \* the parameters that are propagated into Blockchain
- CONSTANTS
- AllNodes
- (* a set of all nodes that can act as validators (correct and faulty) *)
-
- \* the state variables of Blockchain, see Blockchain.tla for the details
- VARIABLES refClock, blockchain, Faulty
-
- \* All the variables of Blockchain. For some reason, BC!vars does not work
- bcvars == <<refClock, blockchain, Faulty>>
-
- (* Create an instance of Blockchain.
- We could write EXTENDS Blockchain, but then all the constants and state variables
- would be hidden inside the Blockchain module.
- *)
- ULTIMATE_HEIGHT == TARGET_HEIGHT + 1
-
- BC == INSTANCE Blockchain_003_draft WITH
- refClock <- refClock, blockchain <- blockchain, Faulty <- Faulty
-
- (************************** Lite client ************************************)
-
- (* the heights on which the light client is working *)
- HEIGHTS == TRUSTED_HEIGHT..TARGET_HEIGHT
-
- (* the control states of the lite client *)
- States == { "working", "finishedSuccess", "finishedFailure" }
-
- \* The verification functions are implemented in the API
- API == INSTANCE LCVerificationApi_003_draft
-
-
- (*
- Initial states of the light client.
- Initially, only the trusted light block is present.
- *)
- LCInit ==
- /\ \E tm \in Int:
- tm >= 0 /\ API!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm
- /\ state = "working"
- /\ nextHeight = TARGET_HEIGHT
- /\ nprobes = 0 \* no tests have been done so far
- /\ LET trustedBlock == blockchain[TRUSTED_HEIGHT]
- trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes]
- IN
- \* initially, fetchedLightBlocks is a function of one element, i.e., TRUSTED_HEIGHT
- /\ fetchedLightBlocks = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
- \* initially, lightBlockStatus is a function of one element, i.e., TRUSTED_HEIGHT
- /\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"]
- \* the latest verified block the the trusted block
- /\ latestVerified = trustedLightBlock
- /\ InitMonitor(trustedLightBlock, trustedLightBlock, localClock, "SUCCESS")
-
- \* block should contain a copy of the block from the reference chain, with a matching commit
- CopyLightBlockFromChain(block, height) ==
- LET ref == blockchain[height]
- lastCommit ==
- IF height < ULTIMATE_HEIGHT
- THEN blockchain[height + 1].lastCommit
- \* for the ultimate block, which we never use, as ULTIMATE_HEIGHT = TARGET_HEIGHT + 1
- ELSE blockchain[height].VS
- IN
- block = [header |-> ref, Commits |-> lastCommit]
-
- \* Either the primary is correct and the block comes from the reference chain,
- \* or the block is produced by a faulty primary.
- \*
- \* [LCV-FUNC-FETCH.1::TLA.1]
- FetchLightBlockInto(block, height) ==
- IF IS_PRIMARY_CORRECT
- THEN CopyLightBlockFromChain(block, height)
- ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block)
-
- \* add a block into the light store
- \*
- \* [LCV-FUNC-UPDATE.1::TLA.1]
- LightStoreUpdateBlocks(lightBlocks, block) ==
- LET ht == block.header.height IN
- [h \in DOMAIN lightBlocks \union {ht} |->
- IF h = ht THEN block ELSE lightBlocks[h]]
-
- \* update the state of a light block
- \*
- \* [LCV-FUNC-UPDATE.1::TLA.1]
- LightStoreUpdateStates(statuses, ht, blockState) ==
- [h \in DOMAIN statuses \union {ht} |->
- IF h = ht THEN blockState ELSE statuses[h]]
-
- \* Check, whether newHeight is a possible next height for the light client.
- \*
- \* [LCV-FUNC-SCHEDULE.1::TLA.1]
- CanScheduleTo(newHeight, pLatestVerified, pNextHeight, pTargetHeight) ==
- LET ht == pLatestVerified.header.height IN
- \/ /\ ht = pNextHeight
- /\ ht < pTargetHeight
- /\ pNextHeight < newHeight
- /\ newHeight <= pTargetHeight
- \/ /\ ht < pNextHeight
- /\ ht < pTargetHeight
- /\ ht < newHeight
- /\ newHeight < pNextHeight
- \/ /\ ht = pTargetHeight
- /\ newHeight = pTargetHeight
-
- \* The loop of VerifyToTarget.
- \*
- \* [LCV-FUNC-MAIN.1::TLA-LOOP.1]
- VerifyToTargetLoop ==
- \* the loop condition is true
- /\ latestVerified.header.height < TARGET_HEIGHT
- \* pick a light block, which will be constrained later
- /\ \E current \in BC!LightBlocks:
- \* Get next LightBlock for verification
- /\ IF nextHeight \in DOMAIN fetchedLightBlocks
- THEN \* copy the block from the light store
- /\ current = fetchedLightBlocks[nextHeight]
- /\ UNCHANGED fetchedLightBlocks
- ELSE \* retrieve a light block and save it in the light store
- /\ FetchLightBlockInto(current, nextHeight)
- /\ fetchedLightBlocks' = LightStoreUpdateBlocks(fetchedLightBlocks, current)
- \* Record that one more probe has been done (for complexity and model checking)
- /\ nprobes' = nprobes + 1
- \* Verify the current block
- /\ LET verdict == API!ValidAndVerified(latestVerified, current, TRUE) IN
- NextMonitor(latestVerified, current, localClock, verdict) /\
- \* Decide whether/how to continue
- CASE verdict = "SUCCESS" ->
- /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified")
- /\ latestVerified' = current
- /\ state' =
- IF latestVerified'.header.height < TARGET_HEIGHT
- THEN "working"
- ELSE "finishedSuccess"
- /\ \E newHeight \in HEIGHTS:
- /\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT)
- /\ nextHeight' = newHeight
-
- [] verdict = "NOT_ENOUGH_TRUST" ->
- (*
- do nothing: the light block current passed validation, but the validator
- set is too different to verify it. We keep the state of
- current at StateUnverified. For a later iteration, Schedule
- might decide to try verification of that light block again.
- *)
- /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateUnverified")
- /\ \E newHeight \in HEIGHTS:
- /\ CanScheduleTo(newHeight, latestVerified, nextHeight, TARGET_HEIGHT)
- /\ nextHeight' = newHeight
- /\ UNCHANGED <<latestVerified, state>>
-
- [] OTHER ->
- \* verdict is some error code
- /\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateFailed")
- /\ state' = "finishedFailure"
- /\ UNCHANGED <<latestVerified, nextHeight>>
-
- \* The terminating condition of VerifyToTarget.
- \*
- \* [LCV-FUNC-MAIN.1::TLA-LOOPCOND.1]
- VerifyToTargetDone ==
- /\ latestVerified.header.height >= TARGET_HEIGHT
- /\ state' = "finishedSuccess"
- /\ UNCHANGED <<nextHeight, nprobes, fetchedLightBlocks, lightBlockStatus, latestVerified>>
- /\ UNCHANGED <<prevVerified, prevCurrent, prevLocalClock, prevVerdict>>
-
- (*
- The local and global clocks can be updated. They can also drift from each other.
- Note that the local clock can actually go backwards in time.
- However, it still stays in the drift envelope
- of [refClock - REAL_CLOCK_DRIFT, refClock + REAL_CLOCK_DRIFT].
- *)
- AdvanceClocks ==
- /\ BC!AdvanceTime
- /\ \E tm \in Int:
- /\ tm >= 0
- /\ API!IsLocalClockWithinDrift(tm, refClock')
- /\ localClock' = tm
- \* if you like the clock to always grow monotonically, uncomment the next line:
- \*/\ localClock' > localClock
-
- (********************* Lite client + Blockchain *******************)
- Init ==
- \* the blockchain is initialized immediately to the ULTIMATE_HEIGHT
- /\ BC!InitToHeight(FAULTY_RATIO)
- \* the light client starts
- /\ LCInit
-
- (*
- The system step is very simple.
- The light client is either executing VerifyToTarget, or it has terminated.
- (In the latter case, a model checker reports a deadlock.)
- Simultaneously, the global clock may advance.
- *)
- Next ==
- /\ state = "working"
- /\ VerifyToTargetLoop \/ VerifyToTargetDone
- /\ AdvanceClocks
-
- (************************* Types ******************************************)
- TypeOK ==
- /\ state \in States
- /\ localClock \in Nat
- /\ refClock \in Nat
- /\ nextHeight \in HEIGHTS
- /\ latestVerified \in BC!LightBlocks
- /\ \E HS \in SUBSET HEIGHTS:
- /\ fetchedLightBlocks \in [HS -> BC!LightBlocks]
- /\ lightBlockStatus
- \in [HS -> {"StateVerified", "StateUnverified", "StateFailed"}]
-
- (************************* Properties ******************************************)
-
- (* The properties to check *)
- \* this invariant candidate is false
- NeverFinish ==
- state = "working"
-
- \* this invariant candidate is false
- NeverFinishNegative ==
- state /= "finishedFailure"
-
- \* This invariant holds true, when the primary is correct.
- \* This invariant candidate is false when the primary is faulty.
- NeverFinishNegativeWhenTrusted ==
- BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
- => state /= "finishedFailure"
-
- \* this invariant candidate is false
- NeverFinishPositive ==
- state /= "finishedSuccess"
-
-
- (**
- Check that the target height has been reached upon successful termination.
- *)
- TargetHeightOnSuccessInv ==
- state = "finishedSuccess" =>
- /\ TARGET_HEIGHT \in DOMAIN fetchedLightBlocks
- /\ lightBlockStatus[TARGET_HEIGHT] = "StateVerified"
-
- (**
- Correctness states that all the obtained headers are exactly like in the blockchain.
-
- It is always the case that every verified header in LightStore was generated by
- an instance of Tendermint consensus.
-
- [LCV-DIST-SAFE.1::CORRECTNESS-INV.1]
- *)
- CorrectnessInv ==
- \A h \in DOMAIN fetchedLightBlocks:
- lightBlockStatus[h] = "StateVerified" =>
- fetchedLightBlocks[h].header = blockchain[h]
-
- (**
- No faulty block was used to construct a proof. This invariant holds,
- only if FAULTY_RATIO < 1/3.
- *)
- NoTrustOnFaultyBlockInv ==
- (state = "finishedSuccess"
- /\ fetchedLightBlocks[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT])
- => CorrectnessInv
-
- (**
- Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "SUCCESS" pairwise
- This property is easily violated, whenever a header cannot be trusted anymore.
- *)
- StoredHeadersAreVerifiedInv ==
- state = "finishedSuccess"
- =>
- \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers
- \/ lh >= rh
- \* either there is a header between them
- \/ \E mh \in DOMAIN fetchedLightBlocks:
- lh < mh /\ mh < rh
- \* or we can verify the right one using the left one
- \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
- fetchedLightBlocks[rh], FALSE)
-
- \* An improved version of StoredHeadersAreVerifiedInv,
- \* assuming that a header may be not trusted.
- \* This invariant candidate is also violated,
- \* as there may be some unverified blocks left in the middle.
- \* This property is violated under two conditions:
- \* (1) the primary is faulty and there are at least 4 blocks,
- \* (2) the primary is correct and there are at least 5 blocks.
- StoredHeadersAreVerifiedOrNotTrustedInv ==
- state = "finishedSuccess"
- =>
- \A lh, rh \in DOMAIN fetchedLightBlocks: \* for every pair of different stored headers
- \/ lh >= rh
- \* either there is a header between them
- \/ \E mh \in DOMAIN fetchedLightBlocks:
- lh < mh /\ mh < rh
- \* or we can verify the right one using the left one
- \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
- fetchedLightBlocks[rh], FALSE)
- \* or the left header is outside the trusting period, so no guarantees
- \/ ~API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header)
-
- (**
- * An improved version of StoredHeadersAreSoundOrNotTrusted,
- * checking the property only for the verified headers.
- * This invariant holds true if CLOCK_DRIFT <= REAL_CLOCK_DRIFT.
- *)
- ProofOfChainOfTrustInv ==
- state = "finishedSuccess"
- =>
- \A lh, rh \in DOMAIN fetchedLightBlocks:
- \* for every pair of stored headers that have been verified
- \/ lh >= rh
- \/ lightBlockStatus[lh] = "StateUnverified"
- \/ lightBlockStatus[rh] = "StateUnverified"
- \* either there is a header between them
- \/ \E mh \in DOMAIN fetchedLightBlocks:
- lh < mh /\ mh < rh /\ lightBlockStatus[mh] = "StateVerified"
- \* or the left header is outside the trusting period, so no guarantees
- \/ ~(API!InTrustingPeriodLocal(fetchedLightBlocks[lh].header))
- \* or we can verify the right one using the left one
- \/ "SUCCESS" = API!ValidAndVerified(fetchedLightBlocks[lh],
- fetchedLightBlocks[rh], FALSE)
-
- (**
- * When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.)
- *)
- NoFailedBlocksOnSuccessInv ==
- state = "finishedSuccess" =>
- \A h \in DOMAIN fetchedLightBlocks:
- lightBlockStatus[h] /= "StateFailed"
-
- \* This property states that whenever the light client finishes with a positive outcome,
- \* the trusted header is still within the trusting period.
- \* We expect this property to be violated. And Apalache shows us a counterexample.
- PositiveBeforeTrustedHeaderExpires ==
- (state = "finishedSuccess") =>
- BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
-
- \* If the primary is correct and the initial trusted block has not expired,
- \* then whenever the algorithm terminates, it reports "success".
- \* This property fails.
- CorrectPrimaryAndTimeliness ==
- (BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
- /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
- state = "finishedSuccess"
-
- (**
- If the primary is correct and there is a trusted block that has not expired,
- then whenever the algorithm terminates, it reports "success".
- This property only holds true, if the local clock is always growing monotonically.
- If the local clock can go backwards in the envelope
- [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT], then the property fails.
-
- [LCV-DIST-LIVE.1::SUCCESS-CORR-PRIMARY-CHAIN-OF-TRUST.1]
- *)
- SuccessOnCorrectPrimaryAndChainOfTrustLocal ==
- (\E h \in DOMAIN fetchedLightBlocks:
- /\ lightBlockStatus[h] = "StateVerified"
- /\ API!InTrustingPeriodLocal(blockchain[h])
- /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
- state = "finishedSuccess"
-
- (**
- Similar to SuccessOnCorrectPrimaryAndChainOfTrust, but using the blockchain clock.
- It fails because the local clock of the client drifted away, so it rejects a block
- that has not expired yet (according to the local clock).
- *)
- SuccessOnCorrectPrimaryAndChainOfTrustGlobal ==
- (\E h \in DOMAIN fetchedLightBlocks:
- lightBlockStatus[h] = "StateVerified" /\ BC!InTrustingPeriod(blockchain[h])
- /\ state /= "working" /\ IS_PRIMARY_CORRECT) =>
- state = "finishedSuccess"
-
- \* Lite Client Completeness: If header h was correctly generated by an instance
- \* of Tendermint consensus (and its age is less than the trusting period),
- \* then the lite client should eventually set trust(h) to true.
- \*
- \* Note that Completeness assumes that the lite client communicates with a correct full node.
- \*
- \* We decompose completeness into Termination (liveness) and Precision (safety).
- \* Once again, Precision is an inverse version of the safety property in Completeness,
- \* as A => B is logically equivalent to ~B => ~A.
- \*
- \* This property holds only when CLOCK_DRIFT = 0 and REAL_CLOCK_DRIFT = 0.
- PrecisionInv ==
- (state = "finishedFailure")
- => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period
- \/ \E h \in DOMAIN fetchedLightBlocks:
- LET lightBlock == fetchedLightBlocks[h] IN
- \* the full node lied to the lite client about the block header
- \/ lightBlock.header /= blockchain[h]
- \* the full node lied to the lite client about the commits
- \/ lightBlock.Commits /= lightBlock.header.VS
-
- \* the old invariant that was found to be buggy by TLC
- PrecisionBuggyInv ==
- (state = "finishedFailure")
- => \/ ~BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT]) \* outside of the trusting period
- \/ \E h \in DOMAIN fetchedLightBlocks:
- LET lightBlock == fetchedLightBlocks[h] IN
- \* the full node lied to the lite client about the block header
- lightBlock.header /= blockchain[h]
-
- \* the worst complexity
- Complexity ==
- LET N == TARGET_HEIGHT - TRUSTED_HEIGHT + 1 IN
- state /= "working" =>
- (2 * nprobes <= N * (N - 1))
-
- (**
- If the light client has terminated, then the expected postcondition holds true.
- *)
- ApiPostInv ==
- state /= "working" =>
- API!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT,
- fetchedLightBlocks, lightBlockStatus,
- TRUSTED_HEIGHT, TARGET_HEIGHT, state)
-
- (*
- We omit termination, as the algorithm deadlocks in the end.
- So termination can be demonstrated by finding a deadlock.
- Of course, one has to analyze the deadlocked state and see that
- the algorithm has indeed terminated there.
- *)
- =============================================================================
- \* Modification History
- \* Last modified Fri Jun 26 12:08:28 CEST 2020 by igor
- \* Created Wed Oct 02 16:39:42 CEST 2019 by igor
|