mirror of
https://github.com/tendermint/tendermint.git
synced 2026-08-17 04:36:11 +00:00
spec: merge rust-spec (#252)
This commit is contained in:
@@ -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 == <<refClock, blockchain, Faulty>>
|
||||
|
||||
(* 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 <<a, b>> 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 <<a, b>> 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 <<blockchain, Faulty>>
|
||||
|
||||
=============================================================================
|
||||
\* 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
|
||||
@@ -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 <<a, b>> 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 == <<blockchain, refClock, Faulty, conflictingBlock, state>>
|
||||
|
||||
\* 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
|
||||
|
||||
==============================================================================
|
||||
@@ -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 <<a, b>> 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)
|
||||
|
||||
|
||||
==================================================================================
|
||||
@@ -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
|
||||
============================================================================
|
||||
@@ -0,0 +1,221 @@
|
||||
|
||||
# Lightclient Attackers Isolation
|
||||
|
||||
> Warning: This is the beginning of an unfinished draft. Don't continue reading!
|
||||
|
||||
Adversarial nodes may have the incentive to lie to a lightclient about the state of a Tendermint blockchain. An attempt to do so is called attack. Light client [verification][verification] checks incoming data by checking a so-called "commit", which is a forwarded set of signed messages that is (supposedly) produced during executing Tendermint consensus. Thus, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules.
|
||||
|
||||
As Tendermint consensus and light client verification is safe under the assumption of more than 2/3 of correct voting power per block [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], this implies that if there was an attack then [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] was violated, that is, there is a block such that
|
||||
|
||||
- validators deviated from the protocol, and
|
||||
- these validators represent more than 1/3 of the voting power in that block.
|
||||
|
||||
In the case of an [attack][node-based-attack-characterization], the lightclient [attack detection mechanism][detection] computes data, so called evidence [[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link], that can be used
|
||||
|
||||
- to proof that there has been attack [[TMBC-LC-EVIDENCE-DATA.1]][TMBC-LC-EVIDENCE-DATA-link] and
|
||||
- as basis to find the actual nodes that deviated from the Tendermint protocol.
|
||||
|
||||
This specification considers how a full node in a Tendermint blockchain can isolate a set of attackers that launched the attack. The set should satisfy
|
||||
|
||||
- the set does not contain a correct validator
|
||||
- the set contains validators that represent more than 1/3 of the voting power of a block that is still within the unbonding period
|
||||
|
||||
# Outline
|
||||
|
||||
**TODO** when preparing a version for broader review.
|
||||
|
||||
# Part I - Basics
|
||||
|
||||
For definitions of data structures used here, in particular LightBlocks [[LCV-DATA-LIGHTBLOCK.1]](https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1), cf. [Light Client Verification][verification].
|
||||
|
||||
# Part II - Definition of the Problem
|
||||
|
||||
The specification of the [detection mechanism][detection] describes
|
||||
|
||||
- what is a light client attack,
|
||||
- conditions under which the detector will detect a light client attack,
|
||||
- and the format of the output data, called evidence, in the case an attack is detected. The format is defined in
|
||||
[[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link] and looks as follows
|
||||
|
||||
```go
|
||||
type LightClientAttackEvidence struct {
|
||||
ConflictingBlock LightBlock
|
||||
CommonHeight int64
|
||||
}
|
||||
```
|
||||
|
||||
The isolator is a function that gets as input evidence `ev`
|
||||
and a prefix of the blockchain `bc` at least up to height `ev.ConflictingBlock.Header.Height + 1`. The output is a set of *peerIDs* of validators.
|
||||
|
||||
We assume that the full node is synchronized with the blockchain and has reached the height `ev.ConflictingBlock.Header.Height + 1`.
|
||||
|
||||
#### **[FN-INV-Output.1]**
|
||||
|
||||
When an output is generated it satisfies the following properties:
|
||||
|
||||
- If
|
||||
- `bc[CommonHeight].bfttime` is within the unbonding period w.r.t. the time at the full node,
|
||||
- `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]`
|
||||
- Validators in `ev.ConflictingBlock.Commit` represent more than 1/3 of the voting power in `bc[ev.CommonHeight].NextValidators`
|
||||
- Then: A set of validators in `bc[CommonHeight].NextValidators` that
|
||||
- represent more than 1/3 of the voting power in `bc[ev.commonHeight].NextValidators`
|
||||
- signed Tendermint consensus messages for height `ev.ConflictingBlock.Header.Height` by violating the Tendermint consensus protocol.
|
||||
- Else: the empty set.
|
||||
|
||||
# Part IV - Protocol
|
||||
|
||||
Here we discuss how to solve the problem of isolating misbehaving processes. We describe the function `isolateMisbehavingProcesses` as well as all the helping functions below. In [Part V](#part-v---Completeness), we discuss why the solution is complete based on result from analysis with automated tools.
|
||||
|
||||
## Isolation
|
||||
|
||||
### Outline
|
||||
|
||||
> Describe solution (in English), decomposition into functions, where communication to other components happens.
|
||||
|
||||
#### **[LCAI-FUNC-MAIN.1]**
|
||||
|
||||
```go
|
||||
func isolateMisbehavingProcesses(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress {
|
||||
|
||||
reference := bc[ev.conflictingBlock.Header.Height].Header
|
||||
ev_header := ev.conflictingBlock.Header
|
||||
|
||||
ref_commit := bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit // + 1 !!
|
||||
ev_commit := ev.conflictingBlock.Commit
|
||||
|
||||
if violatesTMValidity(reference, ev_header) {
|
||||
// lunatic light client attack
|
||||
signatories := Signers(ev.ConflictingBlock.Commit)
|
||||
bonded_vals := Addresses(bc[ev.CommonHeight].NextValidators)
|
||||
return intersection(signatories,bonded_vals)
|
||||
|
||||
}
|
||||
// If this point is reached the validator sets in reference and ev_header are identical
|
||||
else if RoundOf(ref_commit) == RoundOf(ev_commit) {
|
||||
// equivocation light client attack
|
||||
return intersection(Signers(ref_commit), Signers(ev_commit))
|
||||
}
|
||||
else {
|
||||
// amnesia light client attack
|
||||
return IsolateAmnesiaAttacker(ev, bc)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Implementation comment
|
||||
- If the full node has only reached height `ev.conflictingBlock.Header.Height` then `bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit` refers to the locally stored commit for this height. (This commit must be present by the precondition on `length(bc)`.)
|
||||
- We check in the precondition that the unbonding period is not expired. However, since time moves on, before handing the validators over Cosmos SDK, the time needs to be checked again to satisfy the contract which requires that only bonded validators are reported. This passing of validators to the SDK is out of scope of this specification.
|
||||
- Expected precondition
|
||||
- `length(bc) >= ev.conflictingBlock.Header.Height`
|
||||
- `ValidAndVerifiedUnbonding(bc[ev.CommonHeight], ev.ConflictingBlock) == SUCCESS`
|
||||
- `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]`
|
||||
- TODO: input light blocks pass basic validation
|
||||
- Expected postcondition
|
||||
- [[FN-INV-Output.1]](#FN-INV-Output1) holds
|
||||
- Error condition
|
||||
- returns an error if precondition is violated.
|
||||
|
||||
### Details of the Functions
|
||||
|
||||
#### **[LCAI-FUNC-VVU.1]**
|
||||
|
||||
```go
|
||||
func ValidAndVerifiedUnbonding(trusted LightBlock, untrusted LightBlock) Result
|
||||
```
|
||||
|
||||
- Conditions are identical to [[LCV-FUNC-VALID.2]][LCV-FUNC-VALID.link] except the precondition "*trusted.Header.Time > now - trustingPeriod*" is substituted with
|
||||
- `trusted.Header.Time > now - UnbondingPeriod`
|
||||
|
||||
#### **[LCAI-FUNC-NONVALID.1]**
|
||||
|
||||
```go
|
||||
func violatesTMValidity(ref Header, ev Header) boolean
|
||||
```
|
||||
|
||||
- Implementation remarks
|
||||
- checks whether the evidence header `ev` violates the validity property of Tendermint Consensus, by checking agains a reference header
|
||||
- Expected precondition
|
||||
- `ref.Height == ev.Height`
|
||||
- Expected postcondition
|
||||
- returns evaluation of the following disjunction
|
||||
**[[LCAI-NONVALID-OUTPUT.1]]** ==
|
||||
`ref.ValidatorsHash != ev.ValidatorsHash` or
|
||||
`ref.NextValidatorsHash != ev.NextValidatorsHash` or
|
||||
`ref.ConsensusHash != ev.ConsensusHash` or
|
||||
`ref.AppHash != ev.AppHash` or
|
||||
`ref.LastResultsHash != ev.LastResultsHash`
|
||||
|
||||
```go
|
||||
func IsolateAmnesiaAttacker(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Implementation remarks
|
||||
**TODO:** What should we do here? Refer to the accountability doc?
|
||||
- Expected postcondition
|
||||
**TODO:** What should we do here? Refer to the accountability doc?
|
||||
|
||||
```go
|
||||
func RoundOf(commit Commit) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Expected precondition
|
||||
- `commit` is well-formed. In particular all votes are from the same round `r`.
|
||||
- Expected postcondition
|
||||
- returns round `r` that is encoded in all the votes of the commit
|
||||
|
||||
```go
|
||||
func Signers(commit Commit) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Expected postcondition
|
||||
- returns all validator addresses in `commit`
|
||||
|
||||
```go
|
||||
func Addresses(vals Validator[]) ValidatorAddress[]
|
||||
```
|
||||
|
||||
- Expected postcondition
|
||||
- returns all validator addresses in `vals`
|
||||
|
||||
# Part V - Completeness
|
||||
|
||||
As discussed in the beginning of this document, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules.
|
||||
The main function `isolateMisbehavingProcesses` distinguishes three kinds of wrongly signing messages, namely,
|
||||
|
||||
- lunatic: signing invalid blocks
|
||||
- equivocation: double-signing valid blocks in the same consensus round
|
||||
- amnesia: signing conflicting blocks in different consensus rounds, without having seen a quorum of messages that would have allowed to do so.
|
||||
|
||||
The question is whether this captures all attacks.
|
||||
First observe that the first checking in `isolateMisbehavingProcesses` is `violatesTMValidity`. It takes care of lunatic attacks. If this check passes, that is, if `violatesTMValidity` returns `FALSE` this means that [FN-NONVALID-OUTPUT] evaluates to false, which implies that `ref.ValidatorsHash = ev.ValidatorsHash`. Hence after `violatesTMValidity`, all the involved validators are the ones from the blockchain. It is thus sufficient to analyze one instance of Tendermint consensus with a fixed group membership (set of validators). Also it is sufficient to consider two different valid consensus values, that is, binary consensus.
|
||||
|
||||
**TODO** we have analyzed Tendermint consensus with TLA+ and have accompanied Galois in an independent study of the protocol based on [Ivy proofs](https://github.com/tendermint/spec/tree/master/ivy-proofs).
|
||||
|
||||
# References
|
||||
|
||||
[[supervisor]] The specification of the light client supervisor.
|
||||
|
||||
[[verification]] The specification of the light client verification protocol
|
||||
|
||||
[[detection]] The specification of the light client attack detection mechanism.
|
||||
|
||||
[supervisor]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/supervisor/supervisor_001_draft.md
|
||||
|
||||
[verification]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md
|
||||
|
||||
[detection]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md
|
||||
|
||||
[LC-DATA-EVIDENCE-link]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#lc-data-evidence1
|
||||
|
||||
[TMBC-LC-EVIDENCE-DATA-link]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#tmbc-lc-evidence-data1
|
||||
|
||||
[node-based-attack-characterization]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#node-based-characterization-of-attacks
|
||||
|
||||
[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1
|
||||
|
||||
[LCV-FUNC-VALID.link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-valid2
|
||||
@@ -0,0 +1,223 @@
|
||||
# Lightclient Attackers Isolation
|
||||
|
||||
Adversarial nodes may have the incentive to lie to a lightclient about the state of a Tendermint blockchain. An attempt to do so is called attack. Light client [verification][verification] checks incoming data by checking a so-called "commit", which is a forwarded set of signed messages that is (supposedly) produced during executing Tendermint consensus. Thus, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules.
|
||||
|
||||
As Tendermint consensus and light client verification is safe under the assumption of more than 2/3 of correct voting power per block [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], this implies that if there was an attack then [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] was violated, that is, there is a block such that
|
||||
|
||||
- validators deviated from the protocol, and
|
||||
- these validators represent more than 1/3 of the voting power in that block.
|
||||
|
||||
In the case of an [attack][node-based-attack-characterization], the lightclient [attack detection mechanism][detection] computes data, so called evidence [[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link], that can be used
|
||||
|
||||
- to proof that there has been attack [[TMBC-LC-EVIDENCE-DATA.1]][TMBC-LC-EVIDENCE-DATA-link] and
|
||||
- as basis to find the actual nodes that deviated from the Tendermint protocol.
|
||||
|
||||
This specification considers how a full node in a Tendermint blockchain can isolate a set of attackers that launched the attack. The set should satisfy
|
||||
|
||||
- the set does not contain a correct validator
|
||||
- the set contains validators that represent more than 1/3 of the voting power of a block that is still within the unbonding period
|
||||
|
||||
# Outline
|
||||
|
||||
After providing the [problem statement](#Part-I---Basics-and-Definition-of-the-Problem), we specify the [isolator function](#Part-II---Protocol) and close with the discussion about its [correctness](#Part-III---Completeness) which is based on computer-aided analysis of Tendermint Consensus.
|
||||
|
||||
# Part I - Basics and Definition of the Problem
|
||||
|
||||
For definitions of data structures used here, in particular LightBlocks [[LCV-DATA-LIGHTBLOCK.1]](https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1), we refer to the specification of [Light Client Verification][verification].
|
||||
|
||||
The specification of the [detection mechanism][detection] describes
|
||||
|
||||
- what is a light client attack,
|
||||
- conditions under which the detector will detect a light client attack,
|
||||
- and the format of the output data, called evidence, in the case an attack is detected. The format is defined in
|
||||
[[LC-DATA-EVIDENCE.1]][LC-DATA-EVIDENCE-link] and looks as follows
|
||||
|
||||
```go
|
||||
type LightClientAttackEvidence struct {
|
||||
ConflictingBlock LightBlock
|
||||
CommonHeight int64
|
||||
}
|
||||
```
|
||||
|
||||
The isolator is a function that gets as input evidence `ev`
|
||||
and a prefix of the blockchain `bc` at least up to height `ev.ConflictingBlock.Header.Height + 1`. The output is a set of *peerIDs* of validators.
|
||||
|
||||
We assume that the full node is synchronized with the blockchain and has reached the height `ev.ConflictingBlock.Header.Height + 1`.
|
||||
|
||||
#### **[LCAI-INV-Output.1]**
|
||||
|
||||
When an output is generated it satisfies the following properties:
|
||||
|
||||
- If
|
||||
- `bc[CommonHeight].bfttime` is within the unbonding period w.r.t. the time at the full node,
|
||||
- `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]`
|
||||
- Validators in `ev.ConflictingBlock.Commit` represent more than 1/3 of the voting power in `bc[ev.CommonHeight].NextValidators`
|
||||
- Then: The output is a set of validators in `bc[CommonHeight].NextValidators` that
|
||||
- represent more than 1/3 of the voting power in `bc[ev.commonHeight].NextValidators`
|
||||
- signed Tendermint consensus messages for height `ev.ConflictingBlock.Header.Height` by violating the Tendermint consensus protocol.
|
||||
- Else: the empty set.
|
||||
|
||||
# Part II - Protocol
|
||||
|
||||
Here we discuss how to solve the problem of isolating misbehaving processes. We describe the function `isolateMisbehavingProcesses` as well as all the helping functions below. In [Part III](#part-III---Completeness), we discuss why the solution is complete based on result from analysis with automated tools.
|
||||
|
||||
## Isolation
|
||||
|
||||
### Outline
|
||||
|
||||
We first check whether the conflicting block can indeed be verified from the common height. We then first check whether it was a lunatic attack (violating validity). If this is not the case, we check for equivocation. If this also is not the case, we start the on-chain [accountability protocol](https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit).
|
||||
|
||||
#### **[LCAI-FUNC-MAIN.1]**
|
||||
|
||||
```go
|
||||
func isolateMisbehavingProcesses(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress {
|
||||
|
||||
reference := bc[ev.conflictingBlock.Header.Height].Header
|
||||
ev_header := ev.conflictingBlock.Header
|
||||
|
||||
ref_commit := bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit // + 1 !!
|
||||
ev_commit := ev.conflictingBlock.Commit
|
||||
|
||||
if violatesTMValidity(reference, ev_header) {
|
||||
// lunatic light client attack
|
||||
signatories := Signers(ev.ConflictingBlock.Commit)
|
||||
bonded_vals := Addresses(bc[ev.CommonHeight].NextValidators)
|
||||
return intersection(signatories,bonded_vals)
|
||||
|
||||
}
|
||||
// If this point is reached the validator sets in reference and ev_header are identical
|
||||
else if RoundOf(ref_commit) == RoundOf(ev_commit) {
|
||||
// equivocation light client attack
|
||||
return intersection(Signers(ref_commit), Signers(ev_commit))
|
||||
}
|
||||
else {
|
||||
// amnesia light client attack
|
||||
return IsolateAmnesiaAttacker(ev, bc)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Implementation comment
|
||||
- If the full node has only reached height `ev.conflictingBlock.Header.Height` then `bc[ev.conflictingBlock.Header.Height + 1].Header.LastCommit` refers to the locally stored commit for this height. (This commit must be present by the precondition on `length(bc)`.)
|
||||
- We check in the precondition that the unbonding period is not expired. However, since time moves on, before handing the validators over Cosmos SDK, the time needs to be checked again to satisfy the contract which requires that only bonded validators are reported. This passing of validators to the SDK is out of scope of this specification.
|
||||
- Expected precondition
|
||||
- `length(bc) >= ev.conflictingBlock.Header.Height`
|
||||
- `ValidAndVerifiedUnbonding(bc[ev.CommonHeight], ev.ConflictingBlock) == SUCCESS`
|
||||
- `ev.ConflictingBlock.Header != bc[ev.ConflictingBlock.Header.Height]`
|
||||
- `ev.conflictingBlock` satisfies basic validation (in particular all signed messages in the Commit are from the same round)
|
||||
- Expected postcondition
|
||||
- [[FN-INV-Output.1]](#FN-INV-Output1) holds
|
||||
- Error condition
|
||||
- returns an error if precondition is violated.
|
||||
|
||||
### Details of the Functions
|
||||
|
||||
#### **[LCAI-FUNC-VVU.1]**
|
||||
|
||||
```go
|
||||
func ValidAndVerifiedUnbonding(trusted LightBlock, untrusted LightBlock) Result
|
||||
```
|
||||
|
||||
- Conditions are identical to [[LCV-FUNC-VALID.2]][LCV-FUNC-VALID.link] except the precondition "*trusted.Header.Time > now - trustingPeriod*" is substituted with
|
||||
- `trusted.Header.Time > now - UnbondingPeriod`
|
||||
|
||||
#### **[LCAI-FUNC-NONVALID.1]**
|
||||
|
||||
```go
|
||||
func violatesTMValidity(ref Header, ev Header) boolean
|
||||
```
|
||||
|
||||
- Implementation remarks
|
||||
- checks whether the evidence header `ev` violates the validity property of Tendermint Consensus, by checking against a reference header
|
||||
- Expected precondition
|
||||
- `ref.Height == ev.Height`
|
||||
- Expected postcondition
|
||||
- returns evaluation of the following disjunction
|
||||
**[LCAI-NONVALID-OUTPUT.1]** ==
|
||||
`ref.ValidatorsHash != ev.ValidatorsHash` or
|
||||
`ref.NextValidatorsHash != ev.NextValidatorsHash` or
|
||||
`ref.ConsensusHash != ev.ConsensusHash` or
|
||||
`ref.AppHash != ev.AppHash` or
|
||||
`ref.LastResultsHash != ev.LastResultsHash`
|
||||
|
||||
```go
|
||||
func IsolateAmnesiaAttacker(ev LightClientAttackEvidence, bc Blockchain) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Implementation remarks
|
||||
- This triggers the [query/response protocol](https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit).
|
||||
- Expected postcondition
|
||||
- returns attackers according to [LCAI-INV-Output.1].
|
||||
|
||||
```go
|
||||
func RoundOf(commit Commit) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Expected precondition
|
||||
- `commit` is well-formed. In particular all votes are from the same round `r`.
|
||||
- Expected postcondition
|
||||
- returns round `r` that is encoded in all the votes of the commit
|
||||
- Error condition
|
||||
- reports error if precondition is violated
|
||||
|
||||
```go
|
||||
func Signers(commit Commit) []ValidatorAddress
|
||||
```
|
||||
|
||||
- Expected postcondition
|
||||
- returns all validator addresses in `commit`
|
||||
|
||||
```go
|
||||
func Addresses(vals Validator[]) ValidatorAddress[]
|
||||
```
|
||||
|
||||
- Expected postcondition
|
||||
- returns all validator addresses in `vals`
|
||||
|
||||
# Part III - Completeness
|
||||
|
||||
As discussed in the beginning of this document, an attack boils down to creating and signing Tendermint consensus messages in deviation from the Tendermint consensus algorithm rules.
|
||||
The main function `isolateMisbehavingProcesses` distinguishes three kinds of wrongly signed messages, namely,
|
||||
|
||||
- lunatic: signing invalid blocks
|
||||
- equivocation: double-signing valid blocks in the same consensus round
|
||||
- amnesia: signing conflicting blocks in different consensus rounds, without having seen a quorum of messages that would have allowed to do so.
|
||||
|
||||
The question is whether this captures all attacks.
|
||||
First observe that the first check in `isolateMisbehavingProcesses` is `violatesTMValidity`. It takes care of lunatic attacks. If this check passes, that is, if `violatesTMValidity` returns `FALSE` this means that [[LCAI-NONVALID-OUTPUT.1]](#LCAI-FUNC-NONVALID1]) evaluates to false, which implies that `ref.ValidatorsHash = ev.ValidatorsHash`. Hence, after `violatesTMValidity`, all the involved validators are the ones from the blockchain. It is thus sufficient to analyze one instance of Tendermint consensus with a fixed group membership (set of validators). Also, as we have two different blocks for the same height, it is sufficient to consider two different valid consensus values, that is, binary consensus.
|
||||
|
||||
For this fixed group membership, we have analyzed the attacks using the TLA+ specification of [Tendermint Consensus in TLA+][tendermint-accountability]. We checked that indeed the only possible scenarios that can lead to violation of agreement are **equivocation** and **amnesia**. An independent study by Galois of the protocol based on [Ivy proofs](https://github.com/tendermint/spec/tree/master/ivy-proofs) led to the same conclusion.
|
||||
|
||||
# References
|
||||
|
||||
[[supervisor]] The specification of the light client supervisor.
|
||||
|
||||
[[verification]] The specification of the light client verification protocol.
|
||||
|
||||
[[detection]] The specification of the light client attack detection mechanism.
|
||||
|
||||
[[tendermint-accountability]]: TLA+ specification to check the types of attacks
|
||||
|
||||
[tendermint-accountability]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/tendermint-accountability/README.md
|
||||
|
||||
[supervisor]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/supervisor/supervisor_001_draft.md
|
||||
|
||||
[verification]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md
|
||||
|
||||
[detection]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md
|
||||
|
||||
[LC-DATA-EVIDENCE-link]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#lc-data-evidence1
|
||||
|
||||
[TMBC-LC-EVIDENCE-DATA-link]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#tmbc-lc-evidence-data1
|
||||
|
||||
[node-based-attack-characterization]:
|
||||
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md#node-based-characterization-of-attacks
|
||||
|
||||
[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1
|
||||
|
||||
[LCV-FUNC-VALID.link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-valid2
|
||||
@@ -0,0 +1,219 @@
|
||||
|
||||
# Light client attacks
|
||||
|
||||
We define a light client attack as detection of conflicting headers for a given height that can be verified
|
||||
starting from the trusted light block. A light client attack is defined in the context of interactions of
|
||||
light client with two peers. One of the peers (called primary) defines a trace of verified light blocks
|
||||
(primary trace) that are being checked against trace of the other peer (called witness) that we call
|
||||
witness trace.
|
||||
|
||||
A light client attack is defined by the primary and witness traces
|
||||
that have a common root (the same trusted light block for a common height) but forms
|
||||
conflicting branches (end of traces is for the same height but with different headers).
|
||||
Note that conflicting branches could be arbitrarily big as branches continue to diverge after
|
||||
a bifurcation point. We propose an approach that allows us to define a valid light client attack
|
||||
only with a common light block and a single conflicting light block. We rely on the fact that
|
||||
we assume that the primary is under suspicion (therefore not trusted) and that the witness plays
|
||||
support role to detect and process an attack (therefore trusted). Therefore, once a light client
|
||||
detects an attack, it needs to send to a witness only missing data (common height
|
||||
and conflicting light block) as it has its trace. Keeping light client attack data of constant size
|
||||
saves bandwidth and reduces an attack surface. As we will explain below, although in the context of
|
||||
light client core
|
||||
[verification](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/verification)
|
||||
the roles of primary and witness are clearly defined,
|
||||
in case of the attack, we run the same attack detection procedure twice where the roles are swapped.
|
||||
The rationale is that the light client does not know what peer is correct (on a right main branch)
|
||||
so it tries to create and submit an attack evidence to both peers.
|
||||
|
||||
Light client attack evidence consists of a conflicting light block and a common height.
|
||||
|
||||
```go
|
||||
type LightClientAttackEvidence struct {
|
||||
ConflictingBlock LightBlock
|
||||
CommonHeight int64
|
||||
}
|
||||
```
|
||||
|
||||
Full node can validate a light client attack evidence by executing the following procedure:
|
||||
|
||||
```go
|
||||
func IsValid(lcaEvidence LightClientAttackEvidence, bc Blockchain) boolean {
|
||||
commonBlock = GetLightBlock(bc, lcaEvidence.CommonHeight)
|
||||
if commonBlock == nil return false
|
||||
|
||||
// Note that trustingPeriod in ValidAndVerified is set to UNBONDING_PERIOD
|
||||
verdict = ValidAndVerified(commonBlock, lcaEvidence.ConflictingBlock)
|
||||
conflictingHeight = lcaEvidence.ConflictingBlock.Header.Height
|
||||
|
||||
return verdict == OK and bc[conflictingHeight].Header != lcaEvidence.ConflictingBlock.Header
|
||||
}
|
||||
```
|
||||
|
||||
## Light client attack creation
|
||||
|
||||
Given a trusted light block `trusted`, a light node executes the bisection algorithm to verify header
|
||||
`untrusted` at some height `h`. If the bisection algorithm succeeds, then the header `untrusted` is verified.
|
||||
Headers that are downloaded as part of the bisection algorithm are stored in a store and they are also in
|
||||
the verified state. Therefore, after the bisection algorithm successfully terminates we have a trace of
|
||||
the light blocks ([] LightBlock) we obtained from the primary that we call primary trace.
|
||||
|
||||
### Primary trace
|
||||
|
||||
The following invariant holds for the primary trace:
|
||||
|
||||
- Given a `trusted` light block, target height `h`, and `primary_trace` ([] LightBlock):
|
||||
*primary_trace[0] == trusted* and *primary_trace[len(primary_trace)-1].Height == h* and
|
||||
successive light blocks are passing light client verification logic.
|
||||
|
||||
### Witness with a conflicting header
|
||||
|
||||
The verified header at height `h` is cross-checked with every witness as part of
|
||||
[detection](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/detection).
|
||||
If a witness returns the conflicting header at the height `h` the following procedure is executed to verify
|
||||
if the conflicting header comes from the valid trace and if that's the case to create an attack evidence:
|
||||
|
||||
#### Helper functions
|
||||
|
||||
We assume the following helper functions:
|
||||
|
||||
```go
|
||||
// Returns trace of verified light blocks starting from rootHeight and ending with targetHeight.
|
||||
Trace(lightStore LightStore, rootHeight int64, targetHeight int64) LightBlock[]
|
||||
|
||||
// Returns validator set for the given height
|
||||
GetValidators(bc Blockchain, height int64) Validator[]
|
||||
|
||||
// Returns validator set for the given height
|
||||
GetValidators(bc Blockchain, height int64) Validator[]
|
||||
|
||||
// Return validator addresses for the given validators
|
||||
GetAddresses(vals Validator[]) ValidatorAddress[]
|
||||
```
|
||||
|
||||
```go
|
||||
func DetectLightClientAttacks(primary PeerID,
|
||||
primary_trace []LightBlock,
|
||||
witness PeerID) (LightClientAttackEvidence, LightClientAttackEvidence) {
|
||||
primary_lca_evidence, witness_trace = DetectLightClientAttack(primary_trace, witness)
|
||||
|
||||
witness_lca_evidence = nil
|
||||
if witness_trace != nil {
|
||||
witness_lca_evidence, _ = DetectLightClientAttack(witness_trace, primary)
|
||||
}
|
||||
return primary_lca_evidence, witness_lca_evidence
|
||||
}
|
||||
|
||||
func DetectLightClientAttack(trace []LightBlock, peer PeerID) (LightClientAttackEvidence, []LightBlock) {
|
||||
|
||||
lightStore = new LightStore().Update(trace[0], StateTrusted)
|
||||
|
||||
for i in 1..len(trace)-1 {
|
||||
lightStore, result = VerifyToTarget(peer, lightStore, trace[i].Header.Height)
|
||||
|
||||
if result == ResultFailure then return (nil, nil)
|
||||
|
||||
current = lightStore.Get(trace[i].Header.Height)
|
||||
|
||||
// if obtained header is the same as in the trace we continue with a next height
|
||||
if current.Header == trace[i].Header continue
|
||||
|
||||
// we have identified a conflicting header
|
||||
commonBlock = trace[i-1]
|
||||
conflictingBlock = trace[i]
|
||||
|
||||
return (LightClientAttackEvidence { conflictingBlock, commonBlock.Header.Height },
|
||||
Trace(lightStore, trace[i-1].Header.Height, trace[i].Header.Height))
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Evidence handling
|
||||
|
||||
As part of on chain evidence handling, full nodes identifies misbehaving processes and informs
|
||||
the application, so they can be slashed. Note that only bonded validators should
|
||||
be reported to the application. There are three types of attacks that can be executed against
|
||||
Tendermint light client:
|
||||
|
||||
- lunatic attack
|
||||
- equivocation attack and
|
||||
- amnesia attack.
|
||||
|
||||
We now specify the evidence handling logic.
|
||||
|
||||
```go
|
||||
func detectMisbehavingProcesses(lcAttackEvidence LightClientAttackEvidence, bc Blockchain) []ValidatorAddress {
|
||||
assume IsValid(lcaEvidence, bc)
|
||||
|
||||
// lunatic light client attack
|
||||
if !isValidBlock(current.Header, conflictingBlock.Header) {
|
||||
conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
|
||||
bondedValidators = GetNextValidators(bc, lcAttackEvidence.CommonHeight)
|
||||
|
||||
return getSigners(conflictingCommit) intersection GetAddresses(bondedValidators)
|
||||
|
||||
// equivocation light client attack
|
||||
} else if current.Header.Round == conflictingBlock.Header.Round {
|
||||
conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
|
||||
trustedCommit = bc[conflictingBlock.Header.Height+1].LastCommit
|
||||
|
||||
return getSigners(trustedCommit) intersection getSigners(conflictingCommit)
|
||||
|
||||
// amnesia light client attack
|
||||
} else {
|
||||
HandleAmnesiaAttackEvidence(lcAttackEvidence, bc)
|
||||
}
|
||||
}
|
||||
|
||||
// Block validity in this context is defined by the trusted header.
|
||||
func isValidBlock(trusted Header, conflicting Header) boolean {
|
||||
return trusted.ValidatorsHash == conflicting.ValidatorsHash and
|
||||
trusted.NextValidatorsHash == conflicting.NextValidatorsHash and
|
||||
trusted.ConsensusHash == conflicting.ConsensusHash and
|
||||
trusted.AppHash == conflicting.AppHash and
|
||||
trusted.LastResultsHash == conflicting.LastResultsHash
|
||||
}
|
||||
|
||||
func getSigners(commit Commit) []ValidatorAddress {
|
||||
signers = []ValidatorAddress
|
||||
for (i, commitSig) in commit.Signatures {
|
||||
if commitSig.BlockIDFlag == BlockIDFlagCommit {
|
||||
signers.append(commitSig.ValidatorAddress)
|
||||
}
|
||||
}
|
||||
return signers
|
||||
}
|
||||
```
|
||||
|
||||
Note that amnesia attack evidence handling involves more complex processing, i.e., cannot be
|
||||
defined simply on amnesia attack evidence. We explain in the following section a protocol
|
||||
for handling amnesia attack evidence.
|
||||
|
||||
### Amnesia attack evidence handling
|
||||
|
||||
Detecting faulty processes in case of the amnesia attack is more complex and cannot be inferred
|
||||
purely based on attack evidence data. In this case, in order to detect misbehaving processes we need
|
||||
access to votes processes sent/received during the conflicting height. Therefore, amnesia handling assumes that
|
||||
validators persist all votes received and sent during multi-round heights (as amnesia attack
|
||||
is only possible in heights that executes over multiple rounds, i.e., commit round > 0).
|
||||
|
||||
To simplify description of the algorithm we assume existence of the trusted oracle called monitor that will
|
||||
drive the algorithm and output faulty processes at the end. Monitor can be implemented in a
|
||||
distributed setting as on-chain module. The algorithm works as follows:
|
||||
1) Monitor sends votesets request to validators of the conflicting height. Validators
|
||||
are expected to send their votesets within predefined timeout.
|
||||
2) Upon receiving votesets request, validators send their votesets to a monitor.
|
||||
2) Validators which have not sent its votesets within timeout are considered faulty.
|
||||
3) The preprocessing of the votesets is done. That means that the received votesets are analyzed
|
||||
and each vote (valid) sent by process p is added to the voteset of the sender p. This phase ensures that
|
||||
votes sent by faulty processes observed by at least one correct validator cannot be excluded from the analysis.
|
||||
4) Votesets of every validator are analyzed independently to decide whether the validator is correct or faulty.
|
||||
A faulty validators is the one where at least one of those invalid transitions is found:
|
||||
- More than one PREVOTE message is sent in a round
|
||||
- More than one PRECOMMIT message is sent in a round
|
||||
- PRECOMMIT message is sent without receiving +2/3 of voting-power equivalent
|
||||
appropriate PREVOTE messages
|
||||
- PREVOTE message is sent for the value V’ in round r’ and the PRECOMMIT message had
|
||||
been sent for the value V in round r by the same process (r’ > r) and there are no
|
||||
+2/3 of voting-power equivalent PREVOTE(vr, V’) messages (vr ≥ 0 and vr > r and vr < r’)
|
||||
as the justification for sending PREVOTE(r’, V’)
|
||||
Reference in New Issue
Block a user