From 31cfa530821ed2ea71cd0d2c889108c1480f8a64 Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Fri, 11 Dec 2020 15:03:48 +0100 Subject: [PATCH] The TLA+ specification of the attackers detection (#231) * the working attackers isolation spec, needs more comments * the TLA+ spec of the attackers isolation --- .../attacks/Blockchain_003_draft.tla | 166 +++++++++++++++ .../attacks/Isolation_001_draft.tla | 159 +++++++++++++++ .../attacks/LCVerificationApi_003_draft.tla | 192 ++++++++++++++++++ rust-spec/lightclient/attacks/MC_5_3.tla | 18 ++ 4 files changed, 535 insertions(+) create mode 100644 rust-spec/lightclient/attacks/Blockchain_003_draft.tla create mode 100644 rust-spec/lightclient/attacks/Isolation_001_draft.tla create mode 100644 rust-spec/lightclient/attacks/LCVerificationApi_003_draft.tla create mode 100644 rust-spec/lightclient/attacks/MC_5_3.tla diff --git a/rust-spec/lightclient/attacks/Blockchain_003_draft.tla b/rust-spec/lightclient/attacks/Blockchain_003_draft.tla new file mode 100644 index 000000000..fb6e6e8e8 --- /dev/null +++ b/rust-spec/lightclient/attacks/Blockchain_003_draft.tla @@ -0,0 +1,166 @@ +------------------------ MODULE Blockchain_003_draft ----------------------------- +(* + This is a high-level specification of Tendermint blockchain + that is designed specifically for the light client. + Validators have the voting power of one. If you like to model various + voting powers, introduce multiple copies of the same validator + (do not forget to give them unique names though). + *) +EXTENDS Integers, FiniteSets, Apalache + +Min(a, b) == IF a < b THEN a ELSE b + +CONSTANT + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + ULTIMATE_HEIGHT, + (* a maximal height that can be ever reached (modelling artifact) *) + TRUSTING_PERIOD + (* the period within which the validators are trusted *) + +Heights == 1..ULTIMATE_HEIGHT (* possible heights *) + +(* A commit is just a set of nodes who have committed the block *) +Commits == SUBSET AllNodes + +(* The set of all block headers that can be on the blockchain. + This is a simplified version of the Block data structure in the actual implementation. *) +BlockHeaders == [ + height: Heights, + \* the block height + time: Int, + \* the block timestamp in some integer units + lastCommit: Commits, + \* the nodes who have voted on the previous block, the set itself instead of a hash + (* in the implementation, only the hashes of V and NextV are stored in a block, + as V and NextV are stored in the application state *) + VS: SUBSET AllNodes, + \* the validators of this bloc. We store the validators instead of the hash. + NextVS: SUBSET AllNodes + \* the validators of the next block. We store the next validators instead of the hash. +] + +(* A signed header is just a header together with a set of commits *) +LightBlocks == [header: BlockHeaders, Commits: Commits] + +VARIABLES + refClock, + (* the current global time in integer units as perceived by the reference chain *) + blockchain, + (* A sequence of BlockHeaders, which gives us a bird view of the blockchain. *) + Faulty + (* A set of faulty nodes, which can act as validators. We assume that the set + of faulty processes is non-decreasing. If a process has recovered, it should + connect using a different id. *) + +(* all variables, to be used with UNCHANGED *) +vars == <> + +(* The set of all correct nodes in a state *) +Corr == AllNodes \ Faulty + +(* APALACHE annotations *) +a <: b == a \* type annotation + +NT == STRING +NodeSet(S) == S <: {NT} +EmptyNodeSet == NodeSet({}) + +BT == [height |-> Int, time |-> Int, lastCommit |-> {NT}, VS |-> {NT}, NextVS |-> {NT}] + +LBT == [header |-> BT, Commits |-> {NT}] +(* end of APALACHE annotations *) + +(****************************** BLOCKCHAIN ************************************) + +(* the header is still within the trusting period *) +InTrustingPeriod(header) == + refClock < header.time + TRUSTING_PERIOD + +(* + Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes + and pNodes \subseteq D, test whether the set pNodes \subseteq AllNodes has + more than 2/3 of voting power among the nodes in D. + *) +TwoThirds(pVS, pNodes) == + LET TP == Cardinality(pVS) + SP == Cardinality(pVS \intersect pNodes) + IN + 3 * SP > 2 * TP \* when thinking in real numbers, not integers: SP > 2.0 / 3.0 * TP + +(* + Given a set of FaultyNodes, test whether the voting power of the correct nodes in D + is more than 2/3 of the voting power of the faulty nodes in D. + + Parameters: + - pFaultyNodes is a set of nodes that are considered faulty + - pVS is a set of all validators, maybe including Faulty, intersecting with it, etc. + - pMaxFaultRatio is a pair <> that limits the ratio a / b of the faulty + validators from above (exclusive) + *) +FaultyValidatorsFewerThan(pFaultyNodes, pVS, maxRatio) == + LET FN == pFaultyNodes \intersect pVS \* faulty nodes in pNodes + CN == pVS \ pFaultyNodes \* correct nodes in pNodes + CP == Cardinality(CN) \* power of the correct nodes + FP == Cardinality(FN) \* power of the faulty nodes + IN + \* CP + FP = TP is the total voting power + LET TP == CP + FP IN + FP * maxRatio[2] < TP * maxRatio[1] + +(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *) +IsLightBlockAllowedByDigitalSignatures(ht, block) == + \/ block.header = blockchain[ht] \* signed by correct and faulty (maybe) + \/ /\ block.Commits \subseteq Faulty + /\ block.header.height = ht + /\ block.header.time >= 0 \* signed only by faulty + +(* + Initialize the blockchain to the ultimate height right in the initial states. + We pick the faulty validators statically, but that should not affect the light client. + + Parameters: + - pMaxFaultyRatioExclusive is a pair <> that bound the number of + faulty validators in each block by the ratio a / b (exclusive) + *) +InitToHeight(pMaxFaultyRatioExclusive) == + /\ \E Nodes \in SUBSET AllNodes: + Faulty := Nodes \* pick a subset of nodes to be faulty + \* pick the validator sets and last commits + /\ \E vs, lastCommit \in [Heights -> SUBSET AllNodes]: + \E timestamp \in [Heights -> Int]: + \* refClock is at least as early as the timestamp in the last block + /\ \E tm \in Int: + refClock := tm /\ tm >= timestamp[ULTIMATE_HEIGHT] + \* the genesis starts on day 1 + /\ timestamp[1] = 1 + /\ vs[1] = AllNodes + /\ lastCommit[1] = EmptyNodeSet + /\ \A h \in Heights \ {1}: + /\ lastCommit[h] \subseteq vs[h - 1] \* the non-validators cannot commit + /\ TwoThirds(vs[h - 1], lastCommit[h]) \* the commit has >2/3 of validator votes + \* the faulty validators have the power below the threshold + /\ FaultyValidatorsFewerThan(Faulty, vs[h], pMaxFaultyRatioExclusive) + /\ timestamp[h] > timestamp[h - 1] \* the time grows monotonically + /\ timestamp[h] < timestamp[h - 1] + TRUSTING_PERIOD \* but not too fast + \* form the block chain out of validator sets and commits (this makes apalache faster) + /\ blockchain := [h \in Heights |-> + [height |-> h, + time |-> timestamp[h], + VS |-> vs[h], + NextVS |-> IF h < ULTIMATE_HEIGHT THEN vs[h + 1] ELSE AllNodes, + lastCommit |-> lastCommit[h]] + ] \****** + +(********************* BLOCKCHAIN ACTIONS ********************************) +(* + Advance the clock by zero or more time units. + *) +AdvanceTime == + /\ \E tm \in Int: tm >= refClock /\ refClock' = tm + /\ UNCHANGED <> + +============================================================================= +\* Modification History +\* Last modified Wed Jun 10 14:10:54 CEST 2020 by igor +\* Created Fri Oct 11 15:45:11 CEST 2019 by igor diff --git a/rust-spec/lightclient/attacks/Isolation_001_draft.tla b/rust-spec/lightclient/attacks/Isolation_001_draft.tla new file mode 100644 index 000000000..7406b8942 --- /dev/null +++ b/rust-spec/lightclient/attacks/Isolation_001_draft.tla @@ -0,0 +1,159 @@ +----------------------- MODULE Isolation_001_draft ---------------------------- +(** + * The specification of the attackers isolation at full node, + * when it has received an evidence from the light client. + * We check that the isolation spec produces a set of validators + * that have more than 1/3 of the voting power. + * + * It follows the English specification: + * + * https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/attacks/isolate-attackers_001_draft.md + * + * The assumptions made in this specification: + * + * - the voting power of every validator is 1 + * (add more validators, if you need more validators) + * + * - Tendermint security model is violated + * (there are Byzantine validators who signed a conflicting block) + * + * Igor Konnov, Zarko Milosevic, Josef Widder, Informal Systems, 2020 + *) + + +EXTENDS Integers, FiniteSets, Apalache + +\* algorithm parameters +CONSTANTS + AllNodes, + (* a set of all nodes that can act as validators (correct and faulty) *) + COMMON_HEIGHT, + (* an index of the block header that two peers agree upon *) + CONFLICT_HEIGHT, + (* an index of the block header that two peers disagree upon *) + TRUSTING_PERIOD, + (* the period within which the validators are trusted *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + blockchain, (* the chain at the full node *) + refClock, (* the reference clock at the full node *) + Faulty, (* the set of faulty validators *) + conflictingBlock, (* an evidence that two peers reported conflicting blocks *) + state, (* the state of the attack isolation machine at the full node *) + attackers (* the set of the identified attackers *) + +vars == <> + +\* instantiate the chain at the full node +ULTIMATE_HEIGHT == CONFLICT_HEIGHT + 1 +BC == INSTANCE Blockchain_003_draft + +\* use the light client API +TRUSTING_HEIGHT == COMMON_HEIGHT +TARGET_HEIGHT == CONFLICT_HEIGHT + +LC == INSTANCE LCVerificationApi_003_draft + WITH localClock <- refClock, REAL_CLOCK_DRIFT <- 0, CLOCK_DRIFT <- 0 + +\* old-style type annotations in apalache +a <: b == a + +\* [LCAI-NONVALID-OUTPUT.1::TLA.1] +ViolatesValidity(header1, header2) == + \/ header1.VS /= header2.VS + \/ header1.NextVS /= header2.NextVS + \/ header1.height /= header2.height + \/ header1.time /= header2.time + (* The English specification also checks the fields that we do not have + at this level of abstraction: + - header1.ConsensusHash != header2.ConsensusHash or + - header1.AppHash != header2.AppHash or + - header1.LastResultsHash header2 != ev.LastResultsHash + *) + +Init == + /\ state := "init" + \* Pick an arbitrary blockchain from 1 to COMMON_HEIGHT + 1. + /\ BC!InitToHeight(FAULTY_RATIO) \* initializes blockchain, Faulty, and refClock + /\ attackers := {} <: {STRING} \* attackers are unknown + \* Receive an arbitrary evidence. + \* Instantiate the light block fields one by one, + \* to avoid combinatorial explosion of records. + /\ \E time \in Int: + \E VS, NextVS, lastCommit, Commits \in SUBSET AllNodes: + LET conflicting == + [ Commits |-> Commits, + header |-> + [height |-> CONFLICT_HEIGHT, + time |-> time, + VS |-> VS, + NextVS |-> NextVS, + lastCommit |-> lastCommit] ] + IN + LET refBlock == [ header |-> blockchain[COMMON_HEIGHT], + Commits |-> blockchain[COMMON_HEIGHT + 1].lastCommit ] + IN + /\ "SUCCESS" = LC!ValidAndVerifiedUntimed(refBlock, conflicting) + \* More than third of next validators in the common reference block + \* is faulty. That is a precondition for a fork. + /\ 3 * Cardinality(Faulty \intersect refBlock.header.NextVS) + > Cardinality(refBlock.header.NextVS) + \* correct validators cannot sign an invalid block + /\ ViolatesValidity(conflicting.header, refBlock.header) + => conflicting.Commits \subseteq Faulty + /\ conflictingBlock := conflicting + + +\* This is a specification of isolateMisbehavingProcesses. +\* +\* [LCAI-FUNC-MAIN.1::TLA.1] +Next == + /\ state = "init" + \* Extract the rounds from the reference block and the conflicting block. + \* In this specification, we just pick rounds non-deterministically. + \* The English specification calls RoundOf on the blocks. + /\ \E referenceRound, evidenceRound \in Int: + /\ referenceRound >= 0 /\ evidenceRound >= 0 + /\ LET reference == blockchain[CONFLICT_HEIGHT] + referenceCommit == blockchain[CONFLICT_HEIGHT + 1].lastCommit + evidenceHeader == conflictingBlock.header + evidenceCommit == conflictingBlock.Commits + IN + IF ViolatesValidity(reference, evidenceHeader) + THEN /\ attackers' := blockchain[COMMON_HEIGHT].NextVS \intersect evidenceCommit + /\ state' := "Lunatic" + ELSE IF referenceRound = evidenceRound + THEN /\ attackers' := referenceCommit \intersect evidenceCommit + /\ state' := "Equivocation" + ELSE + \* This property is shown in property + \* Accountability of TendermintAcc3.tla + /\ state' := "Amnesia" + /\ \E Attackers \in SUBSET (Faulty \intersect reference.VS): + /\ 3 * Cardinality(Attackers) > Cardinality(reference.VS) + /\ attackers' := Attackers + /\ blockchain' := blockchain + /\ refClock' := refClock + /\ Faulty' := Faulty + /\ conflictingBlock' := conflictingBlock + +(********************************** INVARIANTS *******************************) + +\* This invariant ensure that the attackers have +\* more than 1/3 of the voting power +\* +\* [LCAI-INV-Output.1::TLA-DETECTION-COMPLETENESS.1] +DetectionCompleteness == + state /= "init" => + 3 * Cardinality(attackers) > Cardinality(blockchain[CONFLICT_HEIGHT].VS) + +\* This invariant ensures that only the faulty validators are detected +\* +\* [LCAI-INV-Output.1::TLA-DETECTION-ACCURACY.1] +DetectionAccuracy == + attackers \subseteq Faulty + +============================================================================== diff --git a/rust-spec/lightclient/attacks/LCVerificationApi_003_draft.tla b/rust-spec/lightclient/attacks/LCVerificationApi_003_draft.tla new file mode 100644 index 000000000..909eab92b --- /dev/null +++ b/rust-spec/lightclient/attacks/LCVerificationApi_003_draft.tla @@ -0,0 +1,192 @@ +-------------------- MODULE LCVerificationApi_003_draft -------------------------- +(** + * The common interface of the light client verification and detection. + *) +EXTENDS Integers, FiniteSets + +\* the parameters of Light Client +CONSTANTS + 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) *) + FAULTY_RATIO + (* a pair <> that limits that ratio of faulty validator in the blockchain + from above (exclusive). Tendermint security model prescribes 1 / 3. *) + +VARIABLES + localClock (* current time as measured by the light client *) + +(* the header is still within the trusting period *) +InTrustingPeriodLocal(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - CLOCK_DRIFT + +(* the header is still within the trusting period, even if the clock can go backwards *) +InTrustingPeriodLocalSurely(header) == + \* note that the assumption about the drift reduces the period of trust + localClock < header.time + TRUSTING_PERIOD - 2 * CLOCK_DRIFT + +(* ensure that the local clock does not drift far away from the global clock *) +IsLocalClockWithinDrift(local, global) == + /\ global - REAL_CLOCK_DRIFT <= local + /\ local <= global + REAL_CLOCK_DRIFT + +(** + * Check that the commits in an untrusted block form 1/3 of the next validators + * in a trusted header. + *) +SignedByOneThirdOfTrusted(trusted, untrusted) == + LET TP == Cardinality(trusted.header.NextVS) + SP == Cardinality(untrusted.Commits \intersect trusted.header.NextVS) + IN + 3 * SP > TP + +(** + The first part of the precondition of ValidAndVerified, which does not take + the current time into account. + + [LCV-FUNC-VALID.1::TLA-PRE-UNTIMED.1] + *) +ValidAndVerifiedPreUntimed(trusted, untrusted) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ thdr.height < uhdr.height + \* the trusted block has been created earlier + /\ thdr.time < uhdr.time + /\ untrusted.Commits \subseteq uhdr.VS + /\ LET TP == Cardinality(uhdr.VS) + SP == Cardinality(untrusted.Commits) + IN + 3 * SP > 2 * TP + /\ thdr.height + 1 = uhdr.height => thdr.NextVS = uhdr.VS + (* As we do not have explicit hashes we ignore these three checks of the English spec: + + 1. "trusted.Commit is a commit is for the header trusted.Header, + i.e. it contains the correct hash of the header". + 2. untrusted.Validators = hash(untrusted.Header.Validators) + 3. untrusted.NextValidators = hash(untrusted.Header.NextValidators) + *) + +(** + Check the precondition of ValidAndVerified, including the time checks. + + [LCV-FUNC-VALID.1::TLA-PRE.1] + *) +ValidAndVerifiedPre(trusted, untrusted, checkFuture) == + LET thdr == trusted.header + uhdr == untrusted.header + IN + /\ InTrustingPeriodLocal(thdr) + \* The untrusted block is not from the future (modulo clock drift). + \* Do the check, if it is required. + /\ checkFuture => uhdr.time < localClock + CLOCK_DRIFT + /\ ValidAndVerifiedPreUntimed(trusted, untrusted) + + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + This test does take current time into account, but only looks at the block structure. + + [LCV-FUNC-VALID.1::TLA-UNTIMED.1] + *) +ValidAndVerifiedUntimed(trusted, untrusted) == + IF ~ValidAndVerifiedPreUntimed(trusted, untrusted) + THEN "INVALID" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + +(** + Check, whether an untrusted block is valid and verifiable w.r.t. a trusted header. + + [LCV-FUNC-VALID.1::TLA.1] + *) +ValidAndVerified(trusted, untrusted, checkFuture) == + IF ~ValidAndVerifiedPre(trusted, untrusted, checkFuture) + THEN "INVALID" + ELSE IF ~InTrustingPeriodLocal(untrusted.header) + (* We leave the following test for the documentation purposes. + The implementation should do this test, as signature verification may be slow. + In the TLA+ specification, ValidAndVerified happens in no time. + *) + THEN "FAILED_TRUSTING_PERIOD" + ELSE IF untrusted.header.height = trusted.header.height + 1 + \/ SignedByOneThirdOfTrusted(trusted, untrusted) + THEN "SUCCESS" + ELSE "NOT_ENOUGH_TRUST" + + +(** + The invariant of the light store that is not related to the blockchain + *) +LightStoreInv(fetchedLightBlocks, lightBlockStatus) == + \A lh, rh \in DOMAIN fetchedLightBlocks: + \* for every pair of stored headers that have been verified + \/ lh >= rh + \/ lightBlockStatus[lh] /= "StateVerified" + \/ lightBlockStatus[rh] /= "StateVerified" + \* 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 + \/ LET lhdr == fetchedLightBlocks[lh] + rhdr == fetchedLightBlocks[rh] + IN + \* we can verify the right one using the left one + "SUCCESS" = ValidAndVerifiedUntimed(lhdr, rhdr) + +(** + 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(blockchain, fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] = "StateVerified" => + fetchedLightBlocks[h].header = blockchain[h] + +(** + * When the light client terminates, there are no failed blocks. + * (Otherwise, someone lied to us.) + *) +NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) == + \A h \in DOMAIN fetchedLightBlocks: + lightBlockStatus[h] /= "StateFailed" + +(** + The expected post-condition of VerifyToTarget. + *) +VerifyToTargetPost(blockchain, isPeerCorrect, + fetchedLightBlocks, lightBlockStatus, + trustedHeight, targetHeight, finalState) == + LET trustedHeader == fetchedLightBlocks[trustedHeight].header IN + \* The light client is not lying us on the trusted block. + \* It is straightforward to detect. + /\ lightBlockStatus[trustedHeight] = "StateVerified" + /\ trustedHeight \in DOMAIN fetchedLightBlocks + /\ trustedHeader = blockchain[trustedHeight] + \* the invariants we have found in the light client verification + \* there is a problem with trusting period + /\ isPeerCorrect + => CorrectnessInv(blockchain, fetchedLightBlocks, lightBlockStatus) + \* a correct peer should fail the light client, + \* if the trusted block is in the trusting period + /\ isPeerCorrect /\ InTrustingPeriodLocalSurely(trustedHeader) + => finalState = "finishedSuccess" + /\ finalState = "finishedSuccess" => + /\ lightBlockStatus[targetHeight] = "StateVerified" + /\ targetHeight \in DOMAIN fetchedLightBlocks + /\ NoFailedBlocksOnSuccessInv(fetchedLightBlocks, lightBlockStatus) + /\ LightStoreInv(fetchedLightBlocks, lightBlockStatus) + + +================================================================================== diff --git a/rust-spec/lightclient/attacks/MC_5_3.tla b/rust-spec/lightclient/attacks/MC_5_3.tla new file mode 100644 index 000000000..552de49ae --- /dev/null +++ b/rust-spec/lightclient/attacks/MC_5_3.tla @@ -0,0 +1,18 @@ +------------------------- MODULE MC_5_3 ------------------------------------- + +AllNodes == {"n1", "n2", "n3", "n4", "n5"} +COMMON_HEIGHT == 1 +CONFLICT_HEIGHT == 3 +TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-) +FAULTY_RATIO == <<1, 2>> \* < 1 / 2 faulty validators + +VARIABLES + blockchain, \* the reference blockchain + refClock, \* current time in the reference blockchain + Faulty, \* the set of faulty validators + state, \* the state of the light client detector + conflictingBlock, \* an evidence that two peers reported conflicting blocks + attackers + +INSTANCE Isolation_001_draft +============================================================================