spec: merge rust-spec (#252)

This commit is contained in:
Marko
2021-03-01 08:54:08 +00:00
committed by GitHub
parent 227e5269ca
commit b270ab8d15
84 changed files with 344 additions and 412 deletions
@@ -0,0 +1,10 @@
no;filename;tool;timeout;init;inv;next;args
1;LCD_MC3_3_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10
2;LCD_MC3_3_faulty.tla;apalache;1h;;AccuracyInv;;--length=10
3;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10
4;LCD_MC3_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10
5;LCD_MC3_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10
6;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10
7;LCD_MC4_4_faulty.tla;apalache;1h;;CommonHeightOnEvidenceInv;;--length=10
8;LCD_MC4_4_faulty.tla;apalache;1h;;AccuracyInv;;--length=10
9;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvLocal;;--length=10
1 no filename tool timeout init inv next args
2 1 LCD_MC3_3_faulty.tla apalache 1h CommonHeightOnEvidenceInv --length=10
3 2 LCD_MC3_3_faulty.tla apalache 1h AccuracyInv --length=10
4 3 LCD_MC3_3_faulty.tla apalache 1h PrecisionInvLocal --length=10
5 4 LCD_MC3_4_faulty.tla apalache 1h CommonHeightOnEvidenceInv --length=10
6 5 LCD_MC3_4_faulty.tla apalache 1h AccuracyInv --length=10
7 6 LCD_MC3_4_faulty.tla apalache 1h PrecisionInvLocal --length=10
8 7 LCD_MC4_4_faulty.tla apalache 1h CommonHeightOnEvidenceInv --length=10
9 8 LCD_MC4_4_faulty.tla apalache 1h AccuracyInv --length=10
10 9 LCD_MC4_4_faulty.tla apalache 1h PrecisionInvLocal --length=10
@@ -0,0 +1,4 @@
no;filename;tool;timeout;init;inv;next;args
1;LCD_MC3_3_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10
2;LCD_MC3_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10
3;LCD_MC4_4_faulty.tla;apalache;1h;;PrecisionInvGrayZone;;--length=10
1 no filename tool timeout init inv next args
2 1 LCD_MC3_3_faulty.tla apalache 1h PrecisionInvGrayZone --length=10
3 2 LCD_MC3_4_faulty.tla apalache 1h PrecisionInvGrayZone --length=10
4 3 LCD_MC4_4_faulty.tla apalache 1h PrecisionInvGrayZone --length=10
@@ -0,0 +1,164 @@
------------------------ 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
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) ==
/\ Faulty \in SUBSET AllNodes \* some nodes may fail
\* 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,27 @@
------------------------- MODULE LCD_MC3_3_faulty ---------------------------
AllNodes == {"n1", "n2", "n3"}
TRUSTED_HEIGHT == 1
TARGET_HEIGHT == 3
TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-)
CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting
REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting
IS_PRIMARY_CORRECT == FALSE
IS_SECONDARY_CORRECT == TRUE
FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators
VARIABLES
blockchain, (* the reference blockchain *)
localClock, (* current time in the light client *)
refClock, (* current time in the reference blockchain *)
Faulty, (* the set of faulty validators *)
state, (* the state of the light client detector *)
fetchedLightBlocks1, (* a function from heights to LightBlocks *)
fetchedLightBlocks2, (* a function from heights to LightBlocks *)
fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
commonHeight, (* the height that is trusted in CreateEvidenceForPeer *)
nextHeightToTry, (* the index in CreateEvidenceForPeer *)
evidences
INSTANCE LCDetector_003_draft
============================================================================
@@ -0,0 +1,27 @@
------------------------- MODULE LCD_MC3_4_faulty ---------------------------
AllNodes == {"n1", "n2", "n3"}
TRUSTED_HEIGHT == 1
TARGET_HEIGHT == 4
TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-)
CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting
REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting
IS_PRIMARY_CORRECT == FALSE
IS_SECONDARY_CORRECT == TRUE
FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators
VARIABLES
blockchain, (* the reference blockchain *)
localClock, (* current time in the light client *)
refClock, (* current time in the reference blockchain *)
Faulty, (* the set of faulty validators *)
state, (* the state of the light client detector *)
fetchedLightBlocks1, (* a function from heights to LightBlocks *)
fetchedLightBlocks2, (* a function from heights to LightBlocks *)
fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
commonHeight, (* the height that is trusted in CreateEvidenceForPeer *)
nextHeightToTry, (* the index in CreateEvidenceForPeer *)
evidences
INSTANCE LCDetector_003_draft
============================================================================
@@ -0,0 +1,27 @@
------------------------- MODULE LCD_MC4_4_faulty ---------------------------
AllNodes == {"n1", "n2", "n3", "n4"}
TRUSTED_HEIGHT == 1
TARGET_HEIGHT == 4
TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-)
CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting
REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting
IS_PRIMARY_CORRECT == FALSE
IS_SECONDARY_CORRECT == TRUE
FAULTY_RATIO == <<2, 3>> \* < 2 / 3 faulty validators
VARIABLES
blockchain, (* the reference blockchain *)
localClock, (* current time in the light client *)
refClock, (* current time in the reference blockchain *)
Faulty, (* the set of faulty validators *)
state, (* the state of the light client detector *)
fetchedLightBlocks1, (* a function from heights to LightBlocks *)
fetchedLightBlocks2, (* a function from heights to LightBlocks *)
fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
commonHeight, (* the height that is trusted in CreateEvidenceForPeer *)
nextHeightToTry, (* the index in CreateEvidenceForPeer *)
evidences
INSTANCE LCDetector_003_draft
============================================================================
@@ -0,0 +1,27 @@
------------------------- MODULE LCD_MC5_5_faulty ---------------------------
AllNodes == {"n1", "n2", "n3", "n4", "n5"}
TRUSTED_HEIGHT == 1
TARGET_HEIGHT == 5
TRUSTING_PERIOD == 1400 \* two weeks, one day is 100 time units :-)
CLOCK_DRIFT == 10 \* how much we assume the local clock is drifting
REAL_CLOCK_DRIFT == 3 \* how much the local clock is actually drifting
IS_PRIMARY_CORRECT == FALSE
IS_SECONDARY_CORRECT == TRUE
FAULTY_RATIO == <<2, 3>> \* < 1 / 3 faulty validators
VARIABLES
blockchain, (* the reference blockchain *)
localClock, (* current time in the light client *)
refClock, (* current time in the reference blockchain *)
Faulty, (* the set of faulty validators *)
state, (* the state of the light client detector *)
fetchedLightBlocks1, (* a function from heights to LightBlocks *)
fetchedLightBlocks2, (* a function from heights to LightBlocks *)
fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
commonHeight, (* the height that is trusted in CreateEvidenceForPeer *)
nextHeightToTry, (* the index in CreateEvidenceForPeer *)
evidences
INSTANCE LCDetector_003_draft
============================================================================
@@ -0,0 +1,373 @@
-------------------------- MODULE LCDetector_003_draft -----------------------------
(**
* This is a specification of the light client detector module.
* It follows the English specification:
*
* https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_003_reviewed.md
*
* The assumptions made in this specification:
*
* - light client connects to one primary and one secondary peer
*
* - the light client has its own local clock that can drift from the reference clock
* within the envelope [refClock - CLOCK_DRIFT, refClock + CLOCK_DRIFT].
* The local clock may increase as well as decrease in the the envelope
* (similar to clock synchronization).
*
* - the ratio of the faulty validators is set as the parameter.
*
* Igor Konnov, Josef Widder, 2020
*)
EXTENDS Integers
\* the parameters of Light Client
CONSTANTS
AllNodes,
(* a set of all nodes that can act as validators (correct and faulty) *)
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) *)
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. *)
IS_PRIMARY_CORRECT,
IS_SECONDARY_CORRECT
VARIABLES
blockchain, (* the reference blockchain *)
localClock, (* the local clock of the light client *)
refClock, (* the reference clock in the reference blockchain *)
Faulty, (* the set of faulty validators *)
state, (* the state of the light client detector *)
fetchedLightBlocks1, (* a function from heights to LightBlocks *)
fetchedLightBlocks2, (* a function from heights to LightBlocks *)
fetchedLightBlocks1b, (* a function from heights to LightBlocks *)
commonHeight, (* the height that is trusted in CreateEvidenceForPeer *)
nextHeightToTry, (* the index in CreateEvidenceForPeer *)
evidences (* a set of evidences *)
vars == <<state, blockchain, localClock, refClock, Faulty,
fetchedLightBlocks1, fetchedLightBlocks2, fetchedLightBlocks1b,
commonHeight, nextHeightToTry, evidences >>
\* (old) type annotations in Apalache
a <: b == a
\* instantiate a reference chain
ULTIMATE_HEIGHT == TARGET_HEIGHT + 1
BC == INSTANCE Blockchain_003_draft
WITH ULTIMATE_HEIGHT <- (TARGET_HEIGHT + 1)
\* use the light client API
LC == INSTANCE LCVerificationApi_003_draft
\* evidence type
ET == [peer |-> STRING, conflictingBlock |-> BC!LBT, commonHeight |-> Int]
\* is the algorithm in the terminating state
IsTerminated ==
state \in { <<"NoEvidence", "PRIMARY">>,
<<"NoEvidence", "SECONDARY">>,
<<"FaultyPeer", "PRIMARY">>,
<<"FaultyPeer", "SECONDARY">>,
<<"FoundEvidence", "PRIMARY">> }
(********************************* Initialization ******************************)
\* initialization for the light blocks data structure
InitLightBlocks(lb, Heights) ==
\* BC!LightBlocks is an infinite set, as time is not restricted.
\* Hence, we initialize the light blocks by picking the sets inside.
\E vs, nextVS, lastCommit, commit \in [Heights -> SUBSET AllNodes]:
\* although [Heights -> Int] is an infinite set,
\* Apalache needs just one instance of this set, so it does not complain.
\E timestamp \in [Heights -> Int]:
LET hdr(h) ==
[height |-> h,
time |-> timestamp[h],
VS |-> vs[h],
NextVS |-> nextVS[h],
lastCommit |-> lastCommit[h]]
IN
LET lightHdr(h) ==
[header |-> hdr(h), Commits |-> commit[h]]
IN
lb = [ h \in Heights |-> lightHdr(h) ]
\* initialize the detector algorithm
Init ==
\* initialize the blockchain to TARGET_HEIGHT + 1
/\ BC!InitToHeight(FAULTY_RATIO)
/\ \E tm \in Int:
tm >= 0 /\ LC!IsLocalClockWithinDrift(tm, refClock) /\ localClock = tm
\* start with the secondary looking for evidence
/\ state = <<"Init", "SECONDARY">> /\ commonHeight = 0 /\ nextHeightToTry = 0
/\ evidences = {} <: {ET}
\* Precompute a possible result of light client verification for the primary.
\* It is the input to the detection algorithm.
/\ \E Heights1 \in SUBSET(TRUSTED_HEIGHT..TARGET_HEIGHT):
/\ TRUSTED_HEIGHT \in Heights1
/\ TARGET_HEIGHT \in Heights1
/\ InitLightBlocks(fetchedLightBlocks1, Heights1)
\* As we have a non-deterministic scheduler, for every trace that has
\* an unverified block, there is a filtered trace that only has verified
\* blocks. This is a deep observation.
/\ LET status == [h \in Heights1 |-> "StateVerified"] IN
LC!VerifyToTargetPost(blockchain, IS_PRIMARY_CORRECT,
fetchedLightBlocks1, status,
TRUSTED_HEIGHT, TARGET_HEIGHT, "finishedSuccess")
\* initialize the other data structures to the default values
/\ LET trustedBlock == blockchain[TRUSTED_HEIGHT]
trustedLightBlock == [header |-> trustedBlock, Commits |-> AllNodes]
IN
/\ fetchedLightBlocks2 = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
/\ fetchedLightBlocks1b = [h \in {TRUSTED_HEIGHT} |-> trustedLightBlock]
(********************************* Transitions ******************************)
\* a 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(isPeerCorrect, block, height) ==
IF isPeerCorrect
THEN CopyLightBlockFromChain(block, height)
ELSE BC!IsLightBlockAllowedByDigitalSignatures(height, block)
(**
* Pick the next height, for which there is a block.
*)
PickNextHeight(fetchedBlocks, height) ==
LET largerHeights == { h \in DOMAIN fetchedBlocks: h > height } IN
IF largerHeights = ({} <: {Int})
THEN -1
ELSE CHOOSE h \in largerHeights:
\A h2 \in largerHeights: h <= h2
(**
* Check, whether the target header matches at the secondary and primary.
*)
CompareLast ==
/\ state = <<"Init", "SECONDARY">>
\* fetch a block from the secondary:
\* non-deterministically pick a block that matches the constraints
/\ \E latest \in BC!LightBlocks:
\* for the moment, we ignore the possibility of a timeout when fetching a block
/\ FetchLightBlockInto(IS_SECONDARY_CORRECT, latest, TARGET_HEIGHT)
/\ IF latest.header = fetchedLightBlocks1[TARGET_HEIGHT].header
THEN \* if the headers match, CreateEvidence is not called
/\ state' = <<"NoEvidence", "SECONDARY">>
\* save the retrieved block for further analysis
/\ fetchedLightBlocks2' =
[h \in (DOMAIN fetchedLightBlocks2) \union {TARGET_HEIGHT} |->
IF h = TARGET_HEIGHT THEN latest ELSE fetchedLightBlocks2[h]]
/\ UNCHANGED <<commonHeight, nextHeightToTry>>
ELSE \* prepare the parameters for CreateEvidence
/\ commonHeight' = TRUSTED_HEIGHT
/\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks1, TRUSTED_HEIGHT)
/\ state' = IF nextHeightToTry' >= 0
THEN <<"CreateEvidence", "SECONDARY">>
ELSE <<"FaultyPeer", "SECONDARY">>
/\ UNCHANGED fetchedLightBlocks2
/\ UNCHANGED <<blockchain, Faulty,
fetchedLightBlocks1, fetchedLightBlocks1b, evidences>>
\* the actual loop in CreateEvidence
CreateEvidence(peer, isPeerCorrect, refBlocks, targetBlocks) ==
/\ state = <<"CreateEvidence", peer>>
\* precompute a possible result of light client verification for the secondary
\* we have to introduce HeightRange, because Apalache can only handle a..b
\* for constant a and b
/\ LET HeightRange == { h \in TRUSTED_HEIGHT..TARGET_HEIGHT:
commonHeight <= h /\ h <= nextHeightToTry } IN
\E HeightsRange \in SUBSET(HeightRange):
/\ commonHeight \in HeightsRange /\ nextHeightToTry \in HeightsRange
/\ InitLightBlocks(targetBlocks, HeightsRange)
\* As we have a non-deterministic scheduler, for every trace that has
\* an unverified block, there is a filtered trace that only has verified
\* blocks. This is a deep observation.
/\ \E result \in {"finishedSuccess", "finishedFailure"}:
LET targetStatus == [h \in HeightsRange |-> "StateVerified"] IN
\* call VerifyToTarget for (commonHeight, nextHeightToTry).
/\ LC!VerifyToTargetPost(blockchain, isPeerCorrect,
targetBlocks, targetStatus,
commonHeight, nextHeightToTry, result)
\* case 1: the peer has failed (or the trusting period has expired)
/\ \/ /\ result /= "finishedSuccess"
/\ state' = <<"FaultyPeer", peer>>
/\ UNCHANGED <<commonHeight, nextHeightToTry, evidences>>
\* case 2: success
\/ /\ result = "finishedSuccess"
/\ LET block1 == refBlocks[nextHeightToTry] IN
LET block2 == targetBlocks[nextHeightToTry] IN
IF block1.header /= block2.header
THEN \* the target blocks do not match
/\ state' = <<"FoundEvidence", peer>>
/\ evidences' = evidences \union
{[peer |-> peer,
conflictingBlock |-> block1,
commonHeight |-> commonHeight]}
/\ UNCHANGED <<commonHeight, nextHeightToTry>>
ELSE \* the target blocks match
/\ nextHeightToTry' = PickNextHeight(refBlocks, nextHeightToTry)
/\ commonHeight' = nextHeightToTry
/\ state' = IF nextHeightToTry' >= 0
THEN state
ELSE <<"NoEvidence", peer>>
/\ UNCHANGED evidences
SwitchToPrimary ==
/\ state = <<"FoundEvidence", "SECONDARY">>
/\ nextHeightToTry' = PickNextHeight(fetchedLightBlocks2, commonHeight)
/\ state' = <<"CreateEvidence", "PRIMARY">>
/\ UNCHANGED <<blockchain, refClock, Faulty, localClock,
fetchedLightBlocks1, fetchedLightBlocks2, fetchedLightBlocks1b,
commonHeight, evidences >>
CreateEvidenceForSecondary ==
/\ CreateEvidence("SECONDARY", IS_SECONDARY_CORRECT,
fetchedLightBlocks1, fetchedLightBlocks2')
/\ UNCHANGED <<blockchain, refClock, Faulty, localClock,
fetchedLightBlocks1, fetchedLightBlocks1b>>
CreateEvidenceForPrimary ==
/\ CreateEvidence("PRIMARY", IS_PRIMARY_CORRECT,
fetchedLightBlocks2,
fetchedLightBlocks1b')
/\ UNCHANGED <<blockchain, Faulty,
fetchedLightBlocks1, fetchedLightBlocks2>>
(*
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 ==
/\ \E tm \in Int:
tm >= refClock /\ refClock' = tm
/\ \E tm \in Int:
/\ tm >= localClock
/\ LC!IsLocalClockWithinDrift(tm, refClock')
/\ localClock' = tm
(**
Execute AttackDetector for one secondary.
[LCD-FUNC-DETECTOR.2::LOOP.1]
*)
Next ==
/\ AdvanceClocks
/\ \/ CompareLast
\/ CreateEvidenceForSecondary
\/ SwitchToPrimary
\/ CreateEvidenceForPrimary
\* simple invariants to see the progress of the detector
NeverNoEvidence == state[1] /= "NoEvidence"
NeverFoundEvidence == state[1] /= "FoundEvidence"
NeverFaultyPeer == state[1] /= "FaultyPeer"
NeverCreateEvidence == state[1] /= "CreateEvidence"
NeverFoundEvidencePrimary == state /= <<"FoundEvidence", "PRIMARY">>
NeverReachTargetHeight == nextHeightToTry < TARGET_HEIGHT
EvidenceWhenFaultyInv ==
(state[1] = "FoundEvidence") => (~IS_PRIMARY_CORRECT \/ ~IS_SECONDARY_CORRECT)
NoEvidenceForCorrectInv ==
IS_PRIMARY_CORRECT /\ IS_SECONDARY_CORRECT => evidences = {} <: {ET}
(**
* If we find an evidence by peer A, peer B has ineded given us a corrupted
* header following the common height. Also, we have a verification trace by peer A.
*)
CommonHeightOnEvidenceInv ==
\A e \in evidences:
LET conflicting == e.conflictingBlock IN
LET conflictingHeader == conflicting.header IN
\* the evidence by suspectingPeer can be verified by suspectingPeer in one step
LET SoundEvidence(suspectingPeer, peerBlocks) ==
\/ e.peer /= suspectingPeer
\* the conflicting block from another peer verifies against the common height
\/ /\ "SUCCESS" =
LC!ValidAndVerifiedUntimed(peerBlocks[e.commonHeight], conflicting)
\* and the headers of the same height by the two peers do not match
/\ peerBlocks[conflictingHeader.height].header /= conflictingHeader
IN
/\ SoundEvidence("PRIMARY", fetchedLightBlocks1b)
/\ SoundEvidence("SECONDARY", fetchedLightBlocks2)
(**
* If the light client does not find an evidence,
* then there is no attack on the light client.
*)
AccuracyInv ==
(LC!InTrustingPeriodLocal(fetchedLightBlocks1[TARGET_HEIGHT].header)
/\ state = <<"NoEvidence", "SECONDARY">>)
=>
(fetchedLightBlocks1[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT]
/\ fetchedLightBlocks2[TARGET_HEIGHT].header = blockchain[TARGET_HEIGHT])
(**
* The primary reports a corrupted block at the target height. If the secondary is
* correct and the algorithm has terminated, we should get the evidence.
* This property is violated due to clock drift. VerifyToTarget may fail with
* the correct secondary within the trusting period (due to clock drift, locally
* we think that we are outside of the trusting period).
*)
PrecisionInvGrayZone ==
(/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT]
/\ BC!InTrustingPeriod(blockchain[TRUSTED_HEIGHT])
/\ IS_SECONDARY_CORRECT
/\ IsTerminated)
=>
evidences /= {} <: {ET}
(**
* The primary reports a corrupted block at the target height. If the secondary is
* correct and the algorithm has terminated, we should get the evidence.
* This invariant does not fail, as we are using the local clock to check the trusting
* period.
*)
PrecisionInvLocal ==
(/\ fetchedLightBlocks1[TARGET_HEIGHT].header /= blockchain[TARGET_HEIGHT]
/\ LC!InTrustingPeriodLocalSurely(blockchain[TRUSTED_HEIGHT])
/\ IS_SECONDARY_CORRECT
/\ IsTerminated)
=>
evidences /= {} <: {ET}
====================================================================================
@@ -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)
==================================================================================
+75
View File
@@ -0,0 +1,75 @@
---
order: 1
parent:
title: Fork Detection
order: 2
---
# Tendermint fork detection and IBC fork detection
## Status
This is a work in progress.
This directory captures the ongoing work and discussion on fork
detection both in the context of a Tendermint light node and in the
context of IBC. It contains the following files
### [detection.md](./detection.md)
a draft of the light node fork detection including "proof of fork"
definition, that is, the data structure to submit evidence to full
nodes.
### [discussions.md](./discussions.md)
A collection of ideas and intuitions from recent discussions
- the outcome of recent discussion
- a sketch of the light client supervisor to provide the context in
which fork detection happens
- a discussion about lightstore semantics
### [req-ibc-detection.md](./req-ibc-detection.md)
- a collection of requirements for fork detection in the IBC
context. In particular it contains a section "Required Changes in
ICS 007" with necessary updates to ICS 007 to support Tendermint
fork detection
### [draft-functions.md](./draft-functions.md)
In order to address the collected requirements, we started to sketch
some functions that we will need in the future when we specify in more
detail the
- fork detections
- proof of fork generation
- proof of fork verification
on the following components.
- IBC on-chain components
- Relayer
### TODOs
We decided to merge the files while there are still open points to
address to record the current state an move forward. In particular,
the following points need to be addressed:
- <https://github.com/informalsystems/tendermint-rs/pull/479#discussion_r466504876>
- <https://github.com/informalsystems/tendermint-rs/pull/479#discussion_r466493900>
- <https://github.com/informalsystems/tendermint-rs/pull/479#discussion_r466489045>
- <https://github.com/informalsystems/tendermint-rs/pull/479#discussion_r466491471>
Most likely we will write a specification on the light client
supervisor along the outcomes of
- <https://github.com/informalsystems/tendermint-rs/pull/509>
that also addresses initialization
- <https://github.com/tendermint/spec/issues/131>
@@ -0,0 +1,788 @@
# ***This an unfinished draft. Comments are welcome!***
**TODO:** We will need to do small adaptations to the verification
spec to reflect the semantics in the LightStore (verified, trusted,
untrusted, etc. not needed anymore). In more detail:
- The state of the Lightstore needs to go. Functions like `LatestVerified` can
keep the name but will ignore state as it will not exist anymore.
- verification spec should be adapted to the second parameter of
`VerifyToTarget`
being a lightblock; new version number of function tag;
- We should clarify what is the expectation of VerifyToTarget
so if it returns TimeoutError it can be assumed faulty. I guess that
VerifyToTarget with correct full node should never terminate with
TimeoutError.
- We need to introduce a new version number for the new
specification. So we should decide how
to handle that.
# Light Client Attack Detector
In this specification, we strengthen the light client to be resistant
against so-called light client attacks. In a light client attack, all
the correct Tendermint full nodes agree on the sequence of generated
blocks (no fork), but a set of faulty full nodes attack a light client
by generating (signing) a block that deviates from the block of the
same height on the blockchain. In order to do so, some of these faulty
full nodes must have been validators before and violate
[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link), as otherwise, if
[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) would hold,
[verification](verification) would satisfy
[[LCV-SEQ-SAFE.1]](LCV-SEQ-SAFE-link).
An attack detector (or detector for short) is a mechanism that is used
by the light client [supervisor](supervisor) after
[verification](verification) of a new light block
with the primary, to cross-check the newly learned light block with
other peers (secondaries). It expects as input a light block with some
height *root* (that serves as a root of trust), and a verification
trace (a sequence of lightblocks) that the primary provided.
In case the detector observes a light client attack, it computes
evidence data that can be used by Tendermint full nodes to isolate a
set of faulty full nodes that are still within the unbonding period
(more than 1/3 of the voting power of the validator set at some block of the chain),
and report them via ABCI to the application of a Tendermint blockchain
in order to punish faulty nodes.
## Context of this document
The light client [verification](verification) specification is
designed for the Tendermint failure model (1/3 assumption)
[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link). It is safe under this
assumption, and live if it can reliably (that is, no message loss, no
duplication, and eventually delivered) and timely communicate with a
correct full node. If [[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) assumption is violated, the light client
can be fooled to trust a light block that was not generated by
Tendermint consensus.
This specification, the attack detector, is a "second line of
defense", in case the 1/3 assumption is violated. Its goal is to
detect a light client attack (conflicting light blocks) and collect
evidence. However, it is impractical to probe all full nodes. At this
time we consider a simple scheme of maintaining an address book of
known full nodes from which a small subset (e.g., 4) are chosen
initially to communicate with. More involved book keeping with
probabilistic guarantees can be considered at later stages of the
project.
The light client maintains a simple address book containing addresses
of full nodes that it can pick as primary and secondaries. To obtain
a new light block, the light client first does
[verification](verification) with the primary, and then cross-checks
the light block (and the trace of light blocks that led to it) with
the secondaries using this specification.
## Tendermint Consensus and Light Client Attacks
In this section we will give some mathematical definitions of what we
mean by light client attacks (that are considered in this
specification) and how they differ from main-chain forks. To this end
we start by defining some properties of the sequence of blocks that is
decided upon by Tendermint consensus in normal operation (if the
Tendermint failure model holds
[[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link)),
and then define different
deviations that correspond to attack scenarios.
#### **[TMBC-GENESIS.1]**
Let *Genesis* be the agreed-upon initial block (file).
#### **[TMBC-FUNC-SIGN.1]**
Let *b* and *c* be two light blocks with *b.Header.Height + 1 =
c.Header.Height*. We define the predicate **signs(b,c)** to hold
iff *c.Header.LastCommit* is in *PossibleCommit(b)*.
[[TMBC-SOUND-DISTR-POSS-COMMIT.1]](TMBC-SOUND-DISTR-POSS-COMMIT-link).
> The above encodes sequential verification, that is, intuitively,
> b.Header.NextValidators = c.Header.Validators and 2/3 of
> these Validators signed c?
#### **[TMBC-FUNC-SUPPORT.1]**
Let *b* and *c* be two light blocks. We define the predicate
**supports(b,c,t)** to hold iff
- *t - trustingPeriod < b.Header.Time < t*
- the voting power in *b.NextValidators* of nodes in *c.Commit*
is more than 1/3 of *TotalVotingPower(b.Header.NextValidators)*
> That is, if the [Tendermint failure model](TMBC-FM-2THIRDS-link)
> holds, then *c* has been signed by at least one correct full node, cf.
> [[TMBC-VAL-CONTAINS-CORR.1]](TMBC-VAL-CONTAINS-CORR-link).
> The following formalizes that *b* was properly generated by
> Tendermint; *b* can be traced back to genesis
#### **[TMBC-SEQ-ROOTED.1]**
Let *b* be a light block.
We define *sequ-rooted(b)* iff for all *i*, *1 <= i < h = b.Header.Height*,
there exist light blocks *a(i)* s.t.
- *a(1) = Genesis* and
- *a(h) = b* and
- *signs( a(i) , a(i+1) )*.
> The following formalizes that *c* is trusted based on *b* in
> skipping verification. Observe that we do not require here (yet)
> that *b* was properly generated.
#### **[TMBC-SKIP-TRACE.1]**
Let *b* and *c* be light blocks. We define *skip-trace(b,c,t)* if at
time t there exists an *h* and a sequence *a(1)*, ... *a(h)* s.t.
- *a(1) = b* and
- *a(h) = c* and
- *supports( a(i), a(i+1), t)*, for all i, *1 <= i < h*.
We call such a sequence *a(1)*, ... *a(h)* a **verification trace**.
> The following formalizes that two light blocks of the same height
> should agree on the content of the header. Observe that *b* and *c*
> may disagree on the Commit. This is a special case if the canonical
> commit has not been decided on, that is, if b.Header.Height is the
> maximum height of all blocks decided upon by Tendermint at this
> moment.
#### **[TMBC-SIGN-SKIP-MATCH.1]**
Let *a*, *b*, *c*, be light blocks and *t* a time, we define
*sign-skip-match(a,b,c,t) = true* iff the following implication
evaluates to true:
- *sequ-rooted(a)* and
- *b.Header.Height = c.Header.Height* and
- *skip-trace(a,b,t)*
- *skip-trace(a,c,t)*
implies *b.Header = c.Header*.
> Observe that *sign-skip-match* is defined via an implication. If it
> evaluates to false this means that the left-hand-side of the
> implication evaluates to true, and the right-hand-side evaluates to
> false. In particular, there are two **different** headers *b* and
> *c* that both can be verified from a common block *a* from the
> chain. Thus, the following describes an attack.
#### **[TMBC-ATTACK.1]**
If there exists three light blocks a, b, and c, with
*sign-skip-match(a,b,c,t) = false* then we have an *attack*. We say
we have **an attack at height** *b.Header.Height* and write
*attack(a,b,c,t)*.
> The lightblock *a* need not be unique, that is, there may be
> several blocks that satisfy the above requirement for the same
> blocks *b* and *c*.
[[TMBC-ATTACK.1]](#TMBC-ATTACK1) is a formalization of the violation
of the agreement property based on the result of consensus, that is,
the generated blocks.
**Remark.**
Violation of agreement is only possible if more than 1/3 of the validators (or
next validators) of some previous block deviated from the protocol. The
upcoming "accountability" specification will describe how to compute
a set of at least 1/3 faulty nodes from two conflicting blocks. []
There are different ways to characterize forks
and attack scenarios. This specification uses the "node-based
characterization of attacks" which focuses on what kinds of nodes are
affected (light nodes vs. full nodes). For future reference and
discussion we also provide a
"block-based characterization of attacks" below.
### Node-based characterization of attacks
#### **[TMBC-MC-FORK.1]**
We say there is a (main chain) fork at time *t* if
- there are two correct full nodes *i* and *j* and
- *i* is different from *j* and
- *i* has decided on *b* and
- *j* has decided on *c* and
- there exist *a* such that *attack(a,b,c,t)*.
#### **[TMBC-LC-ATTACK.1]**
We say there is a light client attack at time *t*, if
- there is **no** (main chain) fork [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1), and
- there exist nodes that have computed light blocks *b* and *c* and
- there exist *a* such that *attack(a,b,c,t)*.
We say the attack is at height *a.Header.Height*.
> In this specification we consider detection of light client
> attacks. Intuitively, the case we consider is that
> light block *b* is the one from the
> blockchain, and some attacker has computed *c* and tries to wrongly
> convince
> the light client that *c* is the block from the chain.
#### **[TMBC-LC-ATTACK-EVIDENCE.1]**
We consider the following case of a light client attack
[[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1):
- *attack(a,b,c,t)*
- there is a peer p1 that has a sequence *chain* of blocks from *a* to *b*
- *skip-trace(a,c,t)*: by [[TMBC-SKIP-TRACE.1]](#TMBC-SKIP-TRACE1) there is a
verification trace *v* of the form *a = v(1)*, ... *v(h) = c*
Evidence for p1 (that proves an attack) consists for index i
of v(i) and v(i+1) such that
- E1(i). v(i) is equal to the block of *chain* at height v(i).Height, and
- E2(i). v(i+1) that is different from the block of *chain* at
height v(i+1).height
> Observe p1 can
>
> - check that v(i+1) differs from its block at that height, and
> - verify v(i+1) in one step from v(i) as v is a verification trace.
**Proposition.** In the case of attack, evidence exists.
*Proof.* First observe that
- (A). (NOT E2(i)) implies E1(i+1)
Now by contradiction assume there is no evidence. Thus
- for all i, we have NOT E1(i) or NOT E2(i)
- for i = 1 we have E1(1) and thus NOT E2(1)
thus by induction on i, by (A) we have for all i that **E1(i)**
- from attack we have E2(h-1), and as there is no evidence for
i = h - 1 we get **NOT E1(h-1)**. Contradiction.
QED.
#### **[TMBC-LC-EVIDENCE-DATA.1]**
To prove the attack to p1, because of Point E1, it is sufficient to
submit
- v(i).Height (rather than v(i)).
- v(i+1)
This information is *evidence for height v(i).Height*.
### Block-based characterization of attacks
In this section we provide a different characterization of attacks. It
is not defined on the nodes that are affected but purely on the
content of the blocks. In that sense these definitions are less
operational.
> They might be relevant for a closer analysis of fork scenarios on the
> chain, which is out of the scope of this specification.
#### **[TMBC-SIGN-UNIQUE.1]**
Let *b* and *c* be light blocks, we define the predicate
*sign-unique(b,c)* to evaluate to true iff the following implication
evaluates to true:
- *b.Header.Height = c.Header.Height* and
- *sequ-rooted(b)* and
- *sequ-rooted(c)*
implies *b = c*.
#### **[TMBC-BLOCKS-MCFORK.1]**
If there exists two light blocks b and c, with *sign-unique(b,c) =
false* then we have a *fork*.
> The difference of the above definition to
> [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1) is subtle. The latter requires a
> full node being affected by a bad block while
> [[TMBC-BLOCKS-MCFORK.1]](#TMBC-BLOCKS-MCFORK1) just requires that a
> bad block exists, possibly in memory of an attacker.
> The following captures a light client fork. There is no fork up to
> the height of block b. However, c is of that height, is different,
> and passes skipping verification. It is a stricter property than
> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1), as
> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) requires that no correct full
> node is affected.
#### **[TMBC-BLOCKS-LCFORK.1]**
Let *a*, *b*, *c*, be light blocks and *t* a time. We define
*light-client-fork(a,b,c,t)* iff
- *sign-skip-match(a,b,c,t) = false* and
- *sequ-rooted(b)* and
- *b* is "unique", that is, for all *d*, *sequ-rooted(d)* and
*d.Header.Height = b.Header.Height* implies *d = b*
> Finally, let us also define bogus blocks that have no support.
> Observe that bogus is even defined if there is a fork.
> Also, for the definition it would be sufficient to restrict *a* to
> *a.height < b.height* (which is implied by the definitions which
> unfold until *supports()*).
#### **[TMBC-BOGUS.1]**
Let *b* be a light block and *t* a time. We define *bogus(b,t)* iff
- *sequ-rooted(b) = false* and
- for all *a*, *sequ-rooted(a)* implies *skip-trace(a,b,t) = false*
### Informal Problem statement
There is no sequential specification: the detector only makes sense
in a distributed systems where some nodes misbehave.
We work under the assumption that full nodes and validators are
responsible for detecting attacks on the main chain, and the evidence
reactor takes care of broadcasting evidence to communicate
misbehaving nodes via ABCI to the application, and halt the chain in
case of a fork. The point of this specification is to shield a light
clients against attacks that cannot be detected by full nodes, and
are fully addressed at light clients (and consequently IBC relayers,
which use the light client protocols to observe the state of a
blockchain). In order to provide full nodes the incentive to follow
the protocols when communicating with the light client, this
specification also considers the generation of evidence that will
also be processed by the Tendermint blockchain.
#### **[LCD-IP-MODEL.1]**
The detector is designed under the assumption that
- [[TMBC-FM-2THIRDS]](TMBC-FM-2THIRDS-link) may be violated
- there is no fork on the main chain.
> As a result some faulty full nodes may launch an attack on a light
> client.
The following requirements are operational in that they describe how
things should be done, rather than what should be done. However, they
do not constitute temporal logic verification conditions. For those,
see [LCD-DIST-*] below.
The detector is called in the [supervisor](supervisor) as follows
```go
Evidences := AttackDetector(root_of_trust, verifiedLS);`
```
where
- `root-of-trust` is a light block that is trusted (that is,
except upon initialization, the primary and the secondaries
agreed on in the past), and
- `verifiedLS` is a lightstore that contains a verification trace that
starts from a lightblock that can be verified with the
`root-of-trust` in one step and ends with a lightblock of the height
requested by the user
- `Evidences` is a list of evidences for misbehavior
#### **[LCD-IP-STATEMENT.1]**
Whenever AttackDetector is called, the detector should for each
secondary try to replay the verification trace `verifiedLS` with the
secondary
- in case replaying leads to detection of a light client attack
(one of the lightblocks differ from the one in verifiedLS with
the same height), we should return evidence
- if the secondary cannot provide a verification trace, we have no
proof for an attack. Block *b* may be bogus. In this case the
secondary is faulty and it should be replaced.
## Assumptions/Incentives/Environment
It is not in the interest of faulty full nodes to talk to the
detector as long as the detector is connected to at least one
correct full node. This would only increase the likelihood of
misbehavior being detected. Also we cannot punish them easily
(cheaply). The absence of a response need not be the fault of the full
node.
Correct full nodes have the incentive to respond, because the
detector may help them to understand whether their header is a good
one. We can thus base liveness arguments of the detector on
the assumptions that correct full nodes reliably talk to the
detector.
### Assumptions
#### **[LCD-A-CorrFull.1]**
At all times there is at least one correct full
node among the primary and the secondaries.
> For this version of the detection we take this assumption. It
> allows us to establish the invariant that the lightblock
> `root-of-trust` is always the one from the blockchain, and we can
> use it as starting point for the evidence computation. Moreover, it
> allows us to establish the invariant at the supervisor that any
> lightblock in the (top-level) lightstore is from the blockchain.
> In the future we might design a lightclient based on the assumption
> that at least in regular intervals the lightclient is connected to a
> correct full node. This will require the detector to reconsider
> `root-of-trust`, and remove lightblocks from the top-level
> lightstore.
#### **[LCD-A-RelComm.1]**
Communication between the detector and a correct full node is
reliable and bounded in time. Reliable communication means that
messages are not lost, not duplicated, and eventually delivered. There
is a (known) end-to-end delay *Delta*, such that if a message is sent
at time *t* then it is received and processed by time *t + Delta*.
This implies that we need a timeout of at least *2 Delta* for remote
procedure calls to ensure that the response of a correct peer arrives
before the timeout expires.
## Definitions
### Evidence
Following the definition of
[[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1), by evidence
we refer to a variable of the following type
#### **[LC-DATA-EVIDENCE.1]**
```go
type LightClientAttackEvidence struct {
ConflictingBlock LightBlock
CommonHeight int64
}
```
As the above data is computed for a specific peer, the following
data structure wraps the evidence and adds the peerID.
#### **[LC-DATA-EVIDENCE-INT.1]**
```go
type InternalEvidence struct {
Evidence LightClientAttackEvidence
Peer PeerID
}
```
#### **[LC-SUMBIT-EVIDENCE.1]**
```go
func submitEvidence(Evidences []InternalEvidence)
```
- Expected postcondition
- for each `ev` in `Evidences`: submit `ev.Evidence` to `ev.Peer`
---
### LightStore
Lightblocks and LightStores are defined in the verification
specification [LCV-DATA-LIGHTBLOCK.1] and [LCV-DATA-LIGHTSTORE.1]. See
the [verification specification][verification] for details.
## (Distributed) Problem statement
> As the attack detector is there to reduce the impact of faulty
> nodes, and faulty nodes imply that there is a distributed system,
> there is no sequential specification to which this distributed
> problem statement may refer to.
The detector gets as input a trusted lightblock called *root* and an
auxiliary lightstore called *primary_trace* with lightblocks that have
been verified before, and that were provided by the primary.
#### **[LCD-DIST-INV-ATTACK.1]**
If the detector returns evidence for height *h*
[[TMBC-LC-EVIDENCE-DATA.1]](#TMBC-LC-EVIDENCE-DATA1), then there is an
attack at height *h*. [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1)
#### **[LCD-DIST-INV-STORE.1]**
If the detector does not return evidence, then *primary_trace*
contains only blocks from the blockchain.
#### **[LCD-DIST-LIVE.1]**
The detector eventually terminates.
#### **[LCD-DIST-TERM-NORMAL.1]**
If
- the *primary_trace* contains only blocks from the blockchain, and
- there is no attack, and
- *Secondaries* is always non-empty, and
- the age of *root* is always less than the trusting period,
then the detector does not return evidence.
#### **[LCD-DIST-TERM-ATTACK.1]**
If
- there is an attack, and
- a secondary reports a block that conflicts
with one of the blocks in *primary_trace*, and
- *Secondaries* is always non-empty, and
- the age of *root* is always less than the trusting period,
then the detector returns evidence.
> Observe that above we require that "a secondary reports a block that
> conflicts". If there is an attack, but no secondary tries to launch
> it against the detector (or the message from the secondary is lost
> by the network), then there is nothing to detect for us.
#### **[LCD-DIST-SAFE-SECONDARY.1]**
No correct secondary is ever replaced.
#### **[LCD-DIST-SAFE-BOGUS.1]**
If
- a secondary reports a bogus lightblock,
- the age of *root* is always less than the trusting period,
then the secondary is replaced before the detector terminates.
> The above property is quite operational ("reports"), but it captures
> quite closely the requirement. As the
> detector only makes sense in a distributed setting, and does
> not have a sequential specification, less "pure"
> specification are acceptable.
# Protocol
## Functions and Data defined in other Specifications
### From the supervisor
```go
Replace_Secondary(addr Address, root-of-trust LightBlock)
```
### From the verifier
```go
func VerifyToTarget(primary PeerID, root LightBlock,
targetHeight Height) (LightStore, Result)
```
> Note: the above differs from the current version in the second
> parameter. verification will be revised.
Observe that `VerifyToTarget` does communication with the secondaries
via the function [FetchLightBlock](fetch).
### Shared data of the light client
- a pool of full nodes *FullNodes* that have not been contacted before
- peer set called *Secondaries*
- primary
> Note that the lightStore is not needed to be shared.
## Outline
The problem laid out is solved by calling the function `AttackDetector`
with a lightstore that contains a light block that has just been
verified by the verifier.
Then `AttackDetector` downloads headers from the secondaries. In case
a conflicting header is downloaded from a secondary,
`CreateEvidenceForPeer` which computes evidence in the case that
indeed an attack is confirmed. It could be that the secondary reports
a bogus block, which means that there need not be an attack, and the
secondary is replaced.
## Details of the functions
#### **[LCD-FUNC-DETECTOR.1]:**
```go
func AttackDetector(root LightBlock, primary_trace []LightBlock)
([]InternalEvidence) {
Evidences := new []InternalEvidence;
for each secondary in Secondaries {
// we replay the primary trace with the secondary, in
// order to generate evidence that we can submit to the
// secodary. We return the evidence + the trace the
// secondary told us that spans the evidence at its local store
EvidenceForSecondary, newroot, secondary_trace, result :=
CreateEvidenceForPeer(secondary,
root,
primary_trace);
if result == FaultyPeer {
Replace_Secondary(root);
}
else if result == FoundEvidence {
// the conflict is not bogus
Evidences.Add(EvidenceForSecondary);
// we replay the secondary trace with the primary, ...
EvidenceForPrimary, _, result :=
CreateEvidenceForPeer(primary,
newroot,
secondary_trace);
if result == FoundEvidence {
Evidences.Add(EvidenceForPrimary);
}
// At this point we do not care about the other error
// codes. We already have generated evidence for an
// attack and need to stop the lightclient. It does not
// help to call replace_primary. Also we will use the
// same primary to check with other secondaries in
// later iterations of the loop
}
// In the case where the secondary reports NoEvidence
// we do nothing
}
return Evidences;
}
```
- Expected precondition
- root and primary trace are a verification trace
- Expected postcondition
- solves the problem statement (if attack found, then evidence is reported)
- Error condition
- `ErrorTrustExpired`: fails if root expires (outside trusting
period) [[LCV-INV-TP.1]](LCV-INV-TP1-link)
- `ErrorNoPeers`: if no peers are left to replace secondaries, and
no evidence was found before that happened
---
```go
func CreateEvidenceForPeer(peer PeerID, root LightBlock, trace LightStore)
(Evidence, LightBlock, LightStore, result) {
common := root;
for i in 1 .. len(trace) {
auxLS, result := VerifyToTarget(peer, common, trace[i].Header.Height)
if result != ResultSuccess {
// something went wrong; peer did not provide a verifyable block
return (nil, nil, nil, FaultyPeer)
}
else {
if auxLS.LatestVerified().Header != trace[i].Header {
// the header reported by the peer differs from the
// reference header in trace but both could be
// verified from common in one step.
// we can create evidence for submission to the secondary
ev := new InternalEvidence;
ev.Evidence.ConflictingBlock := trace[i];
ev.Evidence.CommonHeight := common.Height;
ev.Peer := peer
return (ev, common, auxLS, FoundEvidence)
}
else {
// the peer agrees with the trace, we move common forward
// we could delete auxLS as it will be overwritten in
// the next iteration
common := trace[i]
}
}
}
return (nil, nil, nil, NoEvidence)
}
```
- Expected precondition
- root and trace are a verification trace
- Expected postcondition
- finds evidence where trace and peer diverge
- Error condition
- `ErrorTrustExpired`: fails if root expires (outside trusting
period) [[LCV-INV-TP.1]](LCV-INV-TP1-link)
- If `VerifyToTarget` returns error but root is not expired then return
`FaultyPeer`
---
## Correctness arguments
#### Argument for [[LCD-DIST-INV-ATTACK.1]](#LCD-DIST-INV-ATTACK1)
Under the assumption that root and trace are a verification trace,
when in `CreateEvidenceForPeer` the detector the detector creates
evidence, then the lightclient has seen two different headers (one via
`trace` and one via `VerifyToTarget` for the same height that can both
be verified in one step.
#### Argument for [[LCD-DIST-INV-STORE.1]](#LCD-DIST-INV-STORE1)
We assume that there is at least one correct peer, and there is no
fork. As a result the correct peer has the correct sequence of
blocks. Since the primary_trace is checked block-by-block also against
each secondary, and at no point evidence was generated that means at
no point there were conflicting blocks.
#### Argument for [[LCD-DIST-LIVE.1]](#LCD-DIST-LIVE1)
At the latest when [[LCV-INV-TP.1]](LCV-INV-TP1-link) is violated,
`AttackDetector` terminates.
#### Argument for [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1)
As there are finitely many peers, eventually the main loop
terminates. As there is no attack no evidence can be generated.
#### Argument for [[LCD-DIST-TERM-ATTACK.1]](#LCD-DIST-TERM-ATTACK1)
Argument similar to [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1)
#### Argument for [[LCD-DIST-SAFE-SECONDARY.1]](#LCD-DIST-SAFE-SECONDARY1)
Secondaries are only replaced if they time-out or if they report bogus
blocks. The former is ruled out by the timing assumption, the latter
by correct peers only reporting blocks from the chain.
#### Argument for [[LCD-DIST-SAFE-BOGUS.1]](#LCD-DIST-SAFE-BOGUS1)
Once a bogus block is recognized as such the secondary is removed.
# References
> links to other specifications/ADRs this document refers to
[[verification]] The specification of the light client verification.
[[supervisor]] The specification of the light client supervisor.
[verification]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md
[supervisor]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/supervisor/supervisor.md
[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md
[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#tmbc-fm-2thirds1
[TMBC-SOUND-DISTR-POSS-COMMIT-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#tmbc-sound-distr-poss-commit1
[LCV-SEQ-SAFE-link]:https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#lcv-seq-safe1
[TMBC-VAL-CONTAINS-CORR-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#tmbc-val-contains-corr1
[fetch]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#lcv-func-fetch1
[LCV-INV-TP1-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification.md#lcv-inv-tp1
@@ -0,0 +1,831 @@
# Light Client Attack Detector
In this specification, we strengthen the light client to be resistant
against so-called light client attacks. In a light client attack, all
the correct Tendermint full nodes agree on the sequence of generated
blocks (no fork), but a set of faulty full nodes attack a light client
by generating (signing) a block that deviates from the block of the
same height on the blockchain. In order to do so, some of these faulty
full nodes must have been validators before and violate the assumption
of more than two thirds of "correct voting power"
[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link], as otherwise, if
[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] would hold,
[verification][verification] would satisfy
[[LCV-SEQ-SAFE.1]][LCV-SEQ-SAFE-link].
An attack detector (or detector for short) is a mechanism that is used
by the light client [supervisor][supervisor] after
[verification][verification] of a new light block
with the primary, to cross-check the newly learned light block with
other peers (secondaries). It expects as input a light block with some
height *root* (that serves as a root of trust), and a verification
trace (a sequence of lightblocks) that the primary provided.
In case the detector observes a light client attack, it computes
evidence data that can be used by Tendermint full nodes to isolate a
set of faulty full nodes that are still within the unbonding period
(more than 1/3 of the voting power of the validator set at some block
of the chain), and report them via ABCI (application/blockchain
interface)
to the application of a
Tendermint blockchain in order to punish faulty nodes.
## Context of this document
The light client [verification][verification] specification is
designed for the Tendermint failure model (1/3 assumption)
[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link]. It is safe under this
assumption, and live if it can reliably (that is, no message loss, no
duplication, and eventually delivered) and timely communicate with a
correct full node. If [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link]
assumption is violated, the light client can be fooled to trust a
light block that was not generated by Tendermint consensus.
This specification, the attack detector, is a "second line of
defense", in case the 1/3 assumption is violated. Its goal is to
detect a light client attack (conflicting light blocks) and collect
evidence. However, it is impractical to probe all full nodes. At this
time we consider a simple scheme of maintaining an address book of
known full nodes from which a small subset (e.g., 4) are chosen
initially to communicate with. More involved book keeping with
probabilistic guarantees can be considered at later stages of the
project.
The light client maintains a simple address book containing addresses
of full nodes that it can pick as primary and secondaries. To obtain
a new light block, the light client first does
[verification][verification] with the primary, and then cross-checks
the light block (and the trace of light blocks that led to it) with
the secondaries using this specification.
# Outline
- [Part I](#part-i---Tendermint-Consensus-and-Light-Client-Attacks):
Formal definitions of lightclient attacks, based on basic
properties of Tendermint consensus.
- [Node-based characterization of
attacks](#Node-based-characterization-of-attacks). The
definition of attacks used in the problem statement of
this specification.
- [Block-based characterization of attacks](#Block-based-characterization-of-attacks). Alternative definitions
provided for future reference.
- [Part II](#part-ii---problem-statement): Problem statement of
lightclient attack detection
- [Informal Problem Statement](#informal-problem-statement)
- [Assumptions](#Assumptions)
- [Definitions](#definitions)
- [Distributed Problem statement](#Distributed-Problem-statement)
- [Part III](#part-iii---protocol): The protocol
- [Functions and Data defined in other Specifications](#Functions-and-Data-defined-in-other-Specifications)
- [Outline of Solution](#Outline-of-solution)
- [Details of the functions](#Details-of-the-functions)
- [Correctness arguments](#Correctness-arguments)
# Part I - Tendermint Consensus and Light Client Attacks
In this section we will give some mathematical definitions of what we
mean by light client attacks (that are considered in this
specification) and how they differ from main-chain forks. To this end,
we start by defining some properties of the sequence of blocks that is
decided upon by Tendermint consensus in normal operation (if the
Tendermint failure model holds
[[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link]),
and then define different
deviations that correspond to attack scenarios. We consider the notion
of [light blocks][LCV-LB-link] and [headers][LVC-HD-link].
#### **[TMBC-GENESIS.1]**
Let *Genesis* be the agreed-upon initial block (file).
#### **[TMBC-FUNC-SIGN.1]**
Let *b* and *c* be two light blocks with *b.Header.Height + 1 =
c.Header.Height*. We define the predicate **signs(b,c)** to hold
iff *c.Header.LastCommit* is in *PossibleCommit(b)*.
[[TMBC-SOUND-DISTR-POSS-COMMIT.1]][TMBC-SOUND-DISTR-POSS-COMMIT-link].
> The above encodes sequential verification, that is, intuitively,
> b.Header.NextValidators = c.Header.Validators and 2/3 of
> these Validators signed c.
#### **[TMBC-FUNC-SUPPORT.1]**
Let *b* and *c* be two light blocks. We define the predicate
**supports(b,c,t)** to hold iff
- *t - trustingPeriod < b.Header.Time < t*
- the voting power in *b.NextValidators* of nodes in *c.Commit*
is more than 1/3 of *TotalVotingPower(b.Header.NextValidators)*
> That is, if the [Tendermint failure model][TMBC-FM-2THIRDS-link]
> holds, then *c* has been signed by at least one correct full node, cf.
> [[TMBC-VAL-CONTAINS-CORR.1]][TMBC-VAL-CONTAINS-CORR-link].
> The following formalizes that *b* was properly generated by
> Tendermint; *b* can be traced back to genesis.
#### **[TMBC-SEQ-ROOTED.1]**
Let *b* be a light block.
We define *sequ-rooted(b)* iff for all *i*, *1 <= i < h = b.Header.Height*,
there exist light blocks *a(i)* s.t.
- *a(1) = Genesis* and
- *a(h) = b* and
- *signs( a(i) , a(i+1) )*.
> The following formalizes that *c* is trusted based on *b* in
> skipping verification. Observe that we do not require here (yet)
> that *b* was properly generated.
#### **[TMBC-SKIP-TRACE.1]**
Let *b* and *c* be light blocks. We define *skip-trace(b,c,t)* if at
time t there exists an integer *h* and a sequence *a(1)*, ... *a(h)* s.t.
- *a(1) = b* and
- *a(h) = c* and
- *supports( a(i), a(i+1), t)*, for all i, *1 <= i < h*.
We call such a sequence *a(1)*, ... *a(h)* a **verification trace**.
> The following formalizes that two light blocks of the same height
> should agree on the content of the header. Observe that *b* and *c*
> may disagree on the Commit. This is a special case if the canonical
> commit has not been decided on yet, that is, if b.Header.Height is the
> maximum height of all blocks decided upon by Tendermint at this
> moment.
#### **[TMBC-SIGN-SKIP-MATCH.1]**
Let *a*, *b*, *c*, be light blocks and *t* a time, we define
*sign-skip-match(a,b,c,t) = true* iff the following implication
evaluates to true:
- *sequ-rooted(a)* and
- *b.Header.Height = c.Header.Height* and
- *skip-trace(a,b,t)*
- *skip-trace(a,c,t)*
implies *b.Header = c.Header*.
> Observe that *sign-skip-match* is defined via an implication. If it
> evaluates to false this means that the left-hand-side of the
> implication evaluates to true, and the right-hand-side evaluates to
> false. In particular, there are two **different** headers *b* and
> *c* that both can be verified from a common block *a* from the
> chain. Thus, the following describes an attack.
#### **[TMBC-ATTACK.1]**
If there exists three light blocks a, b, and c, with
*sign-skip-match(a,b,c,t) = false* then we have an *attack*. We say
we have **an attack at height** *b.Header.Height* and write
*attack(a,b,c,t)*.
> The lightblock *a* need not be unique, that is, there may be
> several blocks that satisfy the above requirement for the same
> blocks *b* and *c*.
[[TMBC-ATTACK.1]](#TMBC-ATTACK1) is a formalization of the violation
of the agreement property based on the result of consensus, that is,
the generated blocks.
**Remark.**
Violation of agreement is only possible if more than 1/3 of the validators (or
next validators) of some previous block deviated from the protocol. The
upcoming "accountability" specification will describe how to compute
a set of at least 1/3 faulty nodes from two conflicting blocks. []
There are different ways to characterize forks
and attack scenarios. This specification uses the "node-based
characterization of attacks" which focuses on what kinds of nodes are
affected (light nodes vs. full nodes). For future reference and
discussion we also provide a
"block-based characterization of attacks" below.
## Node-based characterization of attacks
#### **[TMBC-MC-FORK.1]**
We say there is a (main chain) fork at time *t* if
- there are two correct full nodes *i* and *j* and
- *i* is different from *j* and
- *i* has decided on *b* and
- *j* has decided on *c* and
- there exist *a* such that *attack(a,b,c,t)*.
#### **[TMBC-LC-ATTACK.1]**
We say there is a light client attack at time *t*, if
- there is **no** (main chain) fork [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1), and
- there exist nodes that have computed light blocks *b* and *c* and
- there exist *a* such that *attack(a,b,c,t)*.
We say the attack is at height *a.Header.Height*.
> In this specification we consider detection of light client
> attacks. Intuitively, the case we consider is that
> light block *b* is the one from the
> blockchain, and some attacker has computed *c* and tries to wrongly
> convince
> the light client that *c* is the block from the chain.
#### **[TMBC-LC-ATTACK-EVIDENCE.1]**
We consider the following case of a light client attack
[[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1):
- *attack(a,b,c,t)*
- there is a peer p1 that has a sequence *chain* of blocks from *a* to *b*
- *skip-trace(a,c,t)*: by [[TMBC-SKIP-TRACE.1]](#TMBC-SKIP-TRACE1) there is a
verification trace *v* of the form *a = v(1)*, ... *v(h) = c*
Evidence for p1 (that proves an attack to p1) consists for index i
of v(i) and v(i+1) such that
- E1(i). v(i) is equal to the block of *chain* at height v(i).Height, and
- E2(i). v(i+1) that is different from the block of *chain* at
height v(i+1).height
> Observe p1 can
>
> - check that v(i+1) differs from its block at that height, and
> - verify v(i+1) in one step from v(i) as v is a verification trace.
#### **[TMBC-LC-EVIDENCE-DATA.1]**
To prove the attack to p1, because of Point E1, it is sufficient to
submit
- v(i).Height (rather than v(i)).
- v(i+1)
This information is *evidence for height v(i).Height*.
## Block-based characterization of attacks
In this section we provide a different characterization of attacks. It
is not defined on the nodes that are affected but purely on the
content of the blocks. In that sense these definitions are less
operational.
> They might be relevant for a closer analysis of fork scenarios on the
> chain, which is out of the scope of this specification.
#### **[TMBC-SIGN-UNIQUE.1]**
Let *b* and *c* be light blocks, we define the predicate
*sign-unique(b,c)* to evaluate to true iff the following implication
evaluates to true:
- *b.Header.Height = c.Header.Height* and
- *sequ-rooted(b)* and
- *sequ-rooted(c)*
implies *b = c*.
#### **[TMBC-BLOCKS-MCFORK.1]**
If there exists two light blocks b and c, with *sign-unique(b,c) =
false* then we have a *fork*.
> The difference of the above definition to
> [[TMBC-MC-FORK.1]](#TMBC-MC-FORK1) is subtle. The latter requires a
> full node being affected by a bad block while
> [[TMBC-BLOCKS-MCFORK.1]](#TMBC-BLOCKS-MCFORK1) just requires that a
> bad block exists, possibly in memory of an attacker.
> The following captures a light client fork. There is no fork up to
> the height of block b. However, c is of that height, is different,
> and passes skipping verification. It is a stricter property than
> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1), as
> [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1) requires that no correct full
> node is affected.
#### **[TMBC-BLOCKS-LCFORK.1]**
Let *a*, *b*, *c*, be light blocks and *t* a time. We define
*light-client-fork(a,b,c,t)* iff
- *sign-skip-match(a,b,c,t) = false* and
- *sequ-rooted(b)* and
- *b* is "unique", that is, for all *d*, *sequ-rooted(d)* and
*d.Header.Height = b.Header.Height* implies *d = b*
> Finally, let us also define bogus blocks that have no support.
> Observe that bogus is even defined if there is a fork.
> Also, for the definition it would be sufficient to restrict *a* to
> *a.height < b.height* (which is implied by the definitions which
> unfold until *supports()*).
#### **[TMBC-BOGUS.1]**
Let *b* be a light block and *t* a time. We define *bogus(b,t)* iff
- *sequ-rooted(b) = false* and
- for all *a*, *sequ-rooted(a)* implies *skip-trace(a,b,t) = false*
# Part II - Problem Statement
## Informal Problem statement
There is no sequential specification: the detector only makes sense
in a distributed systems where some nodes misbehave.
We work under the assumption that full nodes and validators are
responsible for detecting attacks on the main chain, and the evidence
reactor takes care of broadcasting evidence to communicate
misbehaving nodes via ABCI to the application, and halt the chain in
case of a fork. The point of this specification is to shield a light
clients against attacks that cannot be detected by full nodes, and
are fully addressed at light clients (and consequently IBC relayers,
which use the light client protocols to observe the state of a
blockchain). In order to provide full nodes the incentive to follow
the protocols when communicating with the light client, this
specification also considers the generation of evidence that will
also be processed by the Tendermint blockchain.
#### **[LCD-IP-MODEL.1]**
The detector is designed under the assumption that
- [[TMBC-FM-2THIRDS]][TMBC-FM-2THIRDS-link] may be violated
- there is no fork on the main chain.
> As a result some faulty full nodes may launch an attack on a light
> client.
The following requirements are operational in that they describe how
things should be done, rather than what should be done. However, they
do not constitute temporal logic verification conditions. For those,
see [LCD-DIST-*] below.
The detector is called in the [supervisor][supervisor] as follows
```go
Evidences := AttackDetector(root_of_trust, verifiedLS);`
```
where
- `root-of-trust` is a light block that is trusted (that is,
except upon initialization, the primary and the secondaries
agreed on in the past), and
- `verifiedLS` is a lightstore that contains a verification trace that
starts from a lightblock that can be verified with the
`root-of-trust` in one step and ends with a lightblock of the height
requested by the user
- `Evidences` is a list of evidences for misbehavior
#### **[LCD-IP-STATEMENT.1]**
Whenever AttackDetector is called, the detector should for each
secondary cross check the largest header in verifiedLS with the
corresponding header of the same height provided by the secondary. If
there is a deviation, the detector should
try to replay the verification trace `verifiedLS` with the
secondary
- in case replaying leads to detection of a light client attack
(one of the lightblocks differ from the one in verifiedLS with
the same height), we should return evidence
- if the secondary cannot provide a verification trace, we have no
proof for an attack. Block *b* may be bogus. In this case the
secondary is faulty and it should be replaced.
## Assumptions
It is not in the interest of faulty full nodes to talk to the
detector as long as the detector is connected to at least one
correct full node. This would only increase the likelihood of
misbehavior being detected. Also we cannot punish them easily
(cheaply). The absence of a response need not be the fault of the full
node.
Correct full nodes have the incentive to respond, because the
detector may help them to understand whether their header is a good
one. We can thus base liveness arguments of the detector on
the assumptions that correct full nodes reliably talk to the
detector.
#### **[LCD-A-CorrFull.1]**
At all times there is at least one correct full
node among the primary and the secondaries.
> For this version of the detection we take this assumption. It
> allows us to establish the invariant that the lightblock
> `root-of-trust` is always the one from the blockchain, and we can
> use it as starting point for the evidence computation. Moreover, it
> allows us to establish the invariant at the supervisor that any
> lightblock in the (top-level) lightstore is from the blockchain.
> In the future we might design a lightclient based on the assumption
> that at least in regular intervals the lightclient is connected to a
> correct full node. This will require the detector to reconsider
> `root-of-trust`, and remove lightblocks from the top-level
> lightstore.
#### **[LCD-A-RelComm.1]**
Communication between the detector and a correct full node is
reliable and bounded in time. Reliable communication means that
messages are not lost, not duplicated, and eventually delivered. There
is a (known) end-to-end delay *Delta*, such that if a message is sent
at time *t* then it is received and processed by time *t + Delta*.
This implies that we need a timeout of at least *2 Delta* for remote
procedure calls to ensure that the response of a correct peer arrives
before the timeout expires.
## Definitions
### Evidence
Following the definition of
[[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1), by evidence
we refer to a variable of the following type
#### **[LC-DATA-EVIDENCE.1]**
```go
type LightClientAttackEvidence struct {
ConflictingBlock LightBlock
CommonHeight int64
}
```
As the above data is computed for a specific peer, the following
data structure wraps the evidence and adds the peerID.
#### **[LC-DATA-EVIDENCE-INT.1]**
```go
type InternalEvidence struct {
Evidence LightClientAttackEvidence
Peer PeerID
}
```
#### **[LC-SUMBIT-EVIDENCE.1]**
```go
func submitEvidence(Evidences []InternalEvidence)
```
- Expected postcondition
- for each `ev` in `Evidences`: submit `ev.Evidence` to `ev.Peer`
---
### LightStore
Lightblocks and LightStores are defined in the verification
specification [[LCV-DATA-LIGHTBLOCK.1]][LCV-LB-link]
and [[LCV-DATA-LIGHTSTORE.2]][LCV-LS-link]. See
the [verification specification][verification] for details.
## Distributed Problem statement
> As the attack detector is there to reduce the impact of faulty
> nodes, and faulty nodes imply that there is a distributed system,
> there is no sequential specification to which this distributed
> problem statement may refer to.
The detector gets as input a trusted lightblock called *root* and an
auxiliary lightstore called *primary_trace* with lightblocks that have
been verified before, and that were provided by the primary.
#### **[LCD-DIST-INV-ATTACK.1]**
If the detector returns evidence for height *h*
[[TMBC-LC-EVIDENCE-DATA.1]](#TMBC-LC-EVIDENCE-DATA1), then there is an
attack at height *h*. [[TMBC-LC-ATTACK.1]](#TMBC-LC-ATTACK1)
#### **[LCD-DIST-INV-STORE.1]**
If the detector does not return evidence, then *primary_trace*
contains only blocks from the blockchain.
#### **[LCD-DIST-LIVE.1]**
The detector eventually terminates.
#### **[LCD-DIST-TERM-NORMAL.1]**
If
- the *primary_trace* contains only blocks from the blockchain, and
- there is no attack, and
- *Secondaries* is always non-empty, and
- the age of *root* is always less than the trusting period,
then the detector does not return evidence.
#### **[LCD-DIST-TERM-ATTACK.1]**
If
- there is an attack, and
- a secondary reports a block that conflicts
with one of the blocks in *primary_trace*, and
- *Secondaries* is always non-empty, and
- the age of *root* is always less than the trusting period,
then the detector returns evidence.
> Observe that above we require that "a secondary reports a block that
> conflicts". If there is an attack, but no secondary tries to launch
> it against the detector (or the message from the secondary is lost
> by the network), then there is nothing to detect for us.
#### **[LCD-DIST-SAFE-SECONDARY.1]**
No correct secondary is ever replaced.
#### **[LCD-DIST-SAFE-BOGUS.1]**
If
- a secondary reports a bogus lightblock,
- the age of *root* is always less than the trusting period,
then the secondary is replaced before the detector terminates.
> The above property is quite operational (e.g., the usage of
> "reports"), but it captures closely the requirement. As the
> detector only makes sense in a distributed setting, and does not
> have a sequential specification, a less "pure" specification are
> acceptable.
# Part III - Protocol
## Functions and Data defined in other Specifications
### From the [supervisor][supervisor]
[[LC-FUNC-REPLACE-SECONDARY.1]][repl]
```go
Replace_Secondary(addr Address, root-of-trust LightBlock)
```
### From the [verifier][verification]
[[LCV-FUNC-MAIN.2]][vtt]
```go
func VerifyToTarget(primary PeerID, root LightBlock,
targetHeight Height) (LightStore, Result)
```
Observe that `VerifyToTarget` does communication with the secondaries
via the function [FetchLightBlock][fetch].
### Shared data of the light client
- a pool of full nodes *FullNodes* that have not been contacted before
- peer set called *Secondaries*
- primary
> Note that the lightStore is not needed to be shared.
## Outline of solution
The problem laid out is solved by calling the function `AttackDetector`
with a lightstore that contains a light block that has just been
verified by the verifier.
Then `AttackDetector` downloads headers from the secondaries. In case
a conflicting header is downloaded from a secondary, it calls
`CreateEvidenceForPeer` which computes evidence in the case that
indeed an attack is confirmed. It could be that the secondary reports
a bogus block, which means that there need not be an attack, and the
secondary is replaced.
## Details of the functions
#### **[LCD-FUNC-DETECTOR.2]:**
```go
func AttackDetector(root LightBlock, primary_trace []LightBlock)
([]InternalEvidence) {
Evidences := new []InternalEvidence;
for each secondary in Secondaries {
lb, result := FetchLightBlock(secondary,primary_trace.Latest().Header.Height);
if result != ResultSuccess {
Replace_Secondary(root);
}
else if lb.Header != primary_trace.Latest().Header {
// we replay the primary trace with the secondary, in
// order to generate evidence that we can submit to the
// secondary. We return the evidence + the trace the
// secondary told us that spans the evidence at its local store
EvidenceForSecondary, newroot, secondary_trace, result :=
CreateEvidenceForPeer(secondary,
root,
primary_trace);
if result == FaultyPeer {
Replace_Secondary(root);
}
else if result == FoundEvidence {
// the conflict is not bogus
Evidences.Add(EvidenceForSecondary);
// we replay the secondary trace with the primary, ...
EvidenceForPrimary, _, result :=
CreateEvidenceForPeer(primary,
newroot,
secondary_trace);
if result == FoundEvidence {
Evidences.Add(EvidenceForPrimary);
}
// At this point we do not care about the other error
// codes. We already have generated evidence for an
// attack and need to stop the lightclient. It does not
// help to call replace_primary. Also we will use the
// same primary to check with other secondaries in
// later iterations of the loop
}
// In the case where the secondary reports NoEvidence
// after initially it reported a conflicting header.
// secondary is faulty
Replace_Secondary(root);
}
}
return Evidences;
}
```
- Expected precondition
- root and primary trace are a verification trace
- Expected postcondition
- solves the problem statement (if attack found, then evidence is reported)
- Error condition
- `ErrorTrustExpired`: fails if root expires (outside trusting
period) [[LCV-INV-TP.1]][LCV-INV-TP1-link]
- `ErrorNoPeers`: if no peers are left to replace secondaries, and
no evidence was found before that happened
---
```go
func CreateEvidenceForPeer(peer PeerID, root LightBlock, trace LightStore)
(Evidence, LightBlock, LightStore, result) {
common := root;
for i in 1 .. len(trace) {
auxLS, result := VerifyToTarget(peer, common, trace[i].Header.Height)
if result != ResultSuccess {
// something went wrong; peer did not provide a verifiable block
return (nil, nil, nil, FaultyPeer)
}
else {
if auxLS.LatestVerified().Header != trace[i].Header {
// the header reported by the peer differs from the
// reference header in trace but both could be
// verified from common in one step.
// we can create evidence for submission to the secondary
ev := new InternalEvidence;
ev.Evidence.ConflictingBlock := trace[i];
ev.Evidence.CommonHeight := common.Height;
ev.Peer := peer
return (ev, common, auxLS, FoundEvidence)
}
else {
// the peer agrees with the trace, we move common forward.
// we could delete auxLS as it will be overwritten in
// the next iteration
common := trace[i]
}
}
}
return (nil, nil, nil, NoEvidence)
}
```
- Expected precondition
- root and trace are a verification trace
- Expected postcondition
- finds evidence where trace and peer diverge
- Error condition
- `ErrorTrustExpired`: fails if root expires (outside trusting
period) [[LCV-INV-TP.1]][LCV-INV-TP1-link]
- If `VerifyToTarget` returns error but root is not expired then return
`FaultyPeer`
---
## Correctness arguments
#### On the existence of evidence
**Proposition.** In the case of attack,
evidence [[TMBC-LC-ATTACK-EVIDENCE.1]](#TMBC-LC-ATTACK-EVIDENCE1)
exists.
*Proof.* First observe that
- (A). (NOT E2(i)) implies E1(i+1)
Now by contradiction assume there is no evidence. Thus
- for all i, we have NOT E1(i) or NOT E2(i)
- for i = 1 we have E1(1) and thus NOT E2(1)
thus by induction on i, by (A) we have for all i that **E1(i)**
- from attack we have E2(h-1), and as there is no evidence for
i = h - 1 we get **NOT E1(h-1)**. Contradiction.
QED.
#### Argument for [[LCD-DIST-INV-ATTACK.1]](#LCD-DIST-INV-ATTACK1)
Under the assumption that root and trace are a verification trace,
when in `CreateEvidenceForPeer` the detector creates
evidence, then the lightclient has seen two different headers (one via
`trace` and one via `VerifyToTarget`) for the same height that can both
be verified in one step.
#### Argument for [[LCD-DIST-INV-STORE.1]](#LCD-DIST-INV-STORE1)
We assume that there is at least one correct peer, and there is no
fork. As a result, the correct peer has the correct sequence of
blocks. Since the primary_trace is checked block-by-block also against
each secondary, and at no point evidence was generated that means at
no point there were conflicting blocks.
#### Argument for [[LCD-DIST-LIVE.1]](#LCD-DIST-LIVE1)
At the latest when [[LCV-INV-TP.1]][LCV-INV-TP1-link] is violated,
`AttackDetector` terminates.
#### Argument for [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1)
As there are finitely many peers, eventually the main loop
terminates. As there is no attack no evidence can be generated.
#### Argument for [[LCD-DIST-TERM-ATTACK.1]](#LCD-DIST-TERM-ATTACK1)
Argument similar to [[LCD-DIST-TERM-NORMAL.1]](#LCD-DIST-TERM-NORMAL1)
#### Argument for [[LCD-DIST-SAFE-SECONDARY.1]](#LCD-DIST-SAFE-SECONDARY1)
Secondaries are only replaced if they time-out or if they report bogus
blocks. The former is ruled out by the timing assumption, the latter
by correct peers only reporting blocks from the chain.
#### Argument for [[LCD-DIST-SAFE-BOGUS.1]](#LCD-DIST-SAFE-BOGUS1)
Once a bogus block is recognized as such the secondary is removed.
# References
> links to other specifications/ADRs this document refers to
[[verification]] The specification of the light client verification.
[[supervisor]] The specification of the light client supervisor.
[verification]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md
[supervisor]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/supervisor/supervisor_001_draft.md
[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md
[TMBC-FM-2THIRDS-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-fm-2thirds1
[TMBC-SOUND-DISTR-POSS-COMMIT-link]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-sound-distr-poss-commit1
[LCV-SEQ-SAFE-link]:https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-seq-safe1
[TMBC-VAL-CONTAINS-CORR-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-val-contains-corr1
[fetch]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-fetch1
[LCV-INV-TP1-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-inv-tp1
[LCV-LB-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightblock1
[LCV-LS-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-data-lightstore2
[LVC-HD-link]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#tmbc-header-fields2
[repl]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/supervisor/supervisor_001_draft.md#lc-func-replace-secondary1
[vtt]:
https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/verification/verification_002_draft.md#lcv-func-main2
+178
View File
@@ -0,0 +1,178 @@
# Results of Discussions and Decisions
- Generating a minimal proof of fork (as suggested in [Issue #5083](https://github.com/tendermint/tendermint/issues/5083)) is too costly at the light client
- we do not know all lightblocks from the primary
- therefore there are many scenarios. we might even need to ask
the primary again for additional lightblocks to isolate the
branch.
> For instance, the light node starts with block at height 1 and the
> primary provides a block of height 10 that the light node can
> verify immediately. In cross-checking, a secondary now provides a
> conflicting header b10 of height 10 that needs another header b5
> of height 5 to
> verify. Now, in order for the light node to convince the primary:
>
> - The light node cannot just sent b5, as it is not clear whether
> the fork happened before or after 5
> - The light node cannot just send b10, as the primary would also
> need b5 for verification
> - In order to minimize the evidence, the light node may try to
> figure out where the branch happens, e.g., by asking the primary
> for height 5 (it might be that more queries are required, also
> to the secondary. However, assuming that in this scenario the
> primary is faulty it may not respond.
As the main goal is to catch misbehavior of the primary,
evidence generation and punishment must not depend on their
cooperation. So the moment we have proof of fork (even if it
contains several light blocks) we should submit right away.
- decision: "full" proof of fork consists of two traces that originate in the
same lightblock and lead to conflicting headers of the same height.
- For submission of proof of fork, we may do some optimizations, for
instance, we might just submit a trace of lightblocks that verifies a block
different from the one the full node knows (we do not send the trace
the primary gave us back to the primary)
- The light client attack is via the primary. Thus we try to
catch if the primary installs a bad light block
- We do not check secondary against secondary
- For each secondary, we check the primary against one secondary
- Observe that just two blocks for the same height are not
sufficient proof of fork.
One of the blocks may be bogus [TMBC-BOGUS.1] which does
not constitute slashable behavior.
Which leads to the question whether the light node should try to do
fork detection on its initial block (from subjective
initialization). This could be done by doing backwards verification
(with the hashes) until a bifurcation block is found.
While there are scenarios where a
fork could be found, there is also the scenario where a faulty full
node feeds the light node with bogus light blocks and forces the light
node to check hashes until a bogus chain is out of the trusting period.
As a result, the light client
should not try to detect a fork for its initial header. **The initial
header must be trusted as is.**
# Light Client Sequential Supervisor
**TODO:** decide where (into which specification) to put the
following:
We describe the context on which the fork detector is called by giving
a sequential version of the supervisor function.
Roughly, it alternates two phases namely:
- Light Client Verification. As a result, a header of the required
height has been downloaded from and verified with the primary.
- Light Client Fork Detections. As a result the header has been
cross-checked with the secondaries. In case there is a fork we
submit "proof of fork" and exit.
#### **[LC-FUNC-SUPERVISOR.1]:**
```go
func Sequential-Supervisor () (Error) {
loop {
// get the next height
nextHeight := input();
// Verify
result := NoResult;
while result != ResultSuccess {
lightStore,result := VerifyToTarget(primary, lightStore, nextHeight);
if result == ResultFailure {
// pick new primary (promote a secondary to primary)
/// and delete all lightblocks above
// LastTrusted (they have not been cross-checked)
Replace_Primary();
}
}
// Cross-check
PoFs := Forkdetector(lightStore, PoFs);
if PoFs.Empty {
// no fork detected with secondaries, we trust the new
// lightblock
LightStore.Update(testedLB, StateTrusted);
}
else {
// there is a fork, we submit the proofs and exit
for i, p range PoFs {
SubmitProofOfFork(p);
}
return(ErrorFork);
}
}
}
```
**TODO:** finish conditions
- Implementation remark
- Expected precondition
- *lightStore* initialized with trusted header
- *PoFs* empty
- Expected postcondition
- runs forever, or
- is terminated by user and satisfies LightStore invariant, or **TODO**
- has submitted proof of fork upon detecting a fork
- Error condition
- none
----
# Semantics of the LightStore
Currently, a lightblock in the lightstore can be in one of the
following states:
- StateUnverified
- StateVerified
- StateFailed
- StateTrusted
The intuition is that `StateVerified` captures that the lightblock has
been verified with the primary, and `StateTrusted` is the state after
successful cross-checking with the secondaries.
Assuming there is **always one correct node among primary and
secondaries**, and there is no fork on the blockchain, lightblocks that
are in `StateTrusted` can be used by the user with the guarantee of
"finality". If a block in `StateVerified` is used, it might be that
detection later finds a fork, and a roll-back might be needed.
**Remark:** The assumption of one correct node, does not render
verification useless. It is true that if the primary and the
secondaries return the same block we may trust it. However, if there
is a node that provides a different block, the light node still needs
verification to understand whether there is a fork, or whether the
different block is just bogus (without any support of some previous
validator set).
**Remark:** A light node may choose the full nodes it communicates
with (the light node and the full node might even belong to the same
stakeholder) so the assumption might be justified in some cases.
In the future, we will do the following changes
- we assume that only from time to time, the light node is
connected to a correct full node
- this means for some limited time, the light node might have no
means to defend against light client attacks
- as a result we do not have finality
- once the light node reconnects with a correct full node, it
should detect the light client attack and submit evidence.
Under these assumptions, `StateTrusted` loses its meaning. As a
result, it should be removed from the API. We suggest that we replace
it with a flag "trusted" that can be used
- internally for efficiency reasons (to maintain
[LCD-INV-TRUSTED-AGREED.1] until a fork is detected)
- by light client based on the "one correct full node" assumption
----
@@ -0,0 +1,289 @@
# Draft of Functions for Fork detection and Proof of Fork Submisstion
This document collects drafts of function for generating and
submitting proof of fork in the IBC context
- [IBC](#on---chain-ibc-component)
- [Relayer](#relayer)
## On-chain IBC Component
> The following is a suggestions to change the function defined in ICS 007
#### [TAG-IBC-MISBEHAVIOR.1]
```go
func checkMisbehaviourAndUpdateState(cs: ClientState, PoF: LightNodeProofOfFork)
```
**TODO:** finish conditions
- Implementation remark
- Expected precondition
- PoF.TrustedBlock.Header is equal to lightBlock on store with
same height
- both traces end with header of same height
- headers are different
- both traces are supported by PoF.TrustedBlock (`supports`
defined in [TMBC-FUNC]), that is, for `t = currentTimestamp()` (see
ICS 024)
- supports(PoF.TrustedBlock, PoF.PrimaryTrace[1], t)
- supports(PoF.PrimaryTrace[i], PoF.PrimaryTrace[i+1], t) for
*0 < i < length(PoF.PrimaryTrace)*
- supports(PoF.TrustedBlock, PoF.SecondaryTrace[1], t)
- supports(PoF.SecondaryTrace[i], PoF.SecondaryTrace[i+1], t) for
*0 < i < length(PoF.SecondaryTrace)*
- Expected postcondition
- set cs.FrozenHeight to min(cs.FrozenHeight, PoF.TrustedBlock.Header.Height)
- Error condition
- none
----
> The following is a suggestions to add functionality to ICS 002 and 007.
> I suppose the above is the most efficient way to get the required
> information. Another option is to subscribe to "header install"
> events via CosmosSDK
#### [TAG-IBC-HEIGHTS.1]
```go
func QueryHeightsRange(id, from, to) ([]Height)
```
- Expected postcondition
- returns all heights *h*, with *from <= h <= to* for which the
IBC component has a consensus state.
----
> This function can be used if the relayer has no information about
> the IBC component. This allows late-joining relayers to also
> participate in fork dection and the generation in proof of
> fork. Alternatively, we may also postulate that relayers are not
> responsible to detect forks for heights before they started (and
> subscribed to the transactions reporting fresh headers being
> installed at the IBC component).
## Relayer
### Auxiliary Functions to be implemented in the Light Client
#### [LCV-LS-FUNC-GET-PREV.1]
```go
func (ls LightStore) GetPreviousVerified(height Height) (LightBlock, bool)
```
- Expected postcondition
- returns a verified LightBlock, whose height is maximal among all
verified lightblocks with height smaller than `height`
----
### Relayer Submitting Proof of Fork to the IBC Component
There are two ways the relayer can detect a fork
- by the fork detector of one of its lightclients
- be checking the consensus state of the IBC component
The following function ignores how the proof of fork was generated.
It takes a proof of fork as input and computes a proof of fork that
will be accepted by the IBC component.
The problem addressed here is that both, the relayer's light client
and the IBC component have incomplete light stores, that might
not have all light blocks in common.
Hence the relayer has to figure out what the IBC component knows
(intuitively, a meeting point between the two lightstores
computed in `commonRoot`) and compute a proof of fork
(`extendPoF`) that the IBC component will accept based on its
knowledge.
The auxiliary functions `commonRoot` and `extendPoF` are
defined below.
#### [TAG-SUBMIT-POF-IBC.1]
```go
func SubmitIBCProofOfFork(
lightStore LightStore,
PoF: LightNodeProofOfFork,
ibc IBCComponent) (Error) {
if ibc.queryChainConsensusState(PoF.TrustedBlock.Height) = PoF.TrustedBlock {
// IBC component has root of PoF on store, we can just submit
ibc.submitMisbehaviourToClient(ibc.id,PoF)
return Success
// note sure about the id parameter
}
else {
// the ibc component does not have the TrustedBlock and might
// even be on yet a different branch. We have to compute a PoF
// that the ibc component can verifiy based on its current
// knowledge
ibcLightBlock, lblock, _, result := commonRoot(lightStore, ibc, PoF.TrustedBlock)
if result = Success {
newPoF = extendPoF(ibcLightBlock, lblock, lightStore, PoF)
ibc.submitMisbehaviourToClient(ibc.id, newPoF)
return Success
}
else{
return CouldNotGeneratePoF
}
}
}
```
**TODO:** finish conditions
- Implementation remark
- Expected precondition
- Expected postcondition
- Error condition
- none
----
### Auxiliary Functions at the Relayer
> If the relayer detects a fork, it has to compute a proof of fork that
> will convince the IBC component. That is it has to compare the
> relayer's local lightstore against the lightstore of the IBC
> component, and find common ancestor lightblocks.
#### [TAG-COMMON-ROOT.1]
```go
func commonRoot(lightStore LightStore, ibc IBCComponent, lblock
LightBlock) (LightBlock, LightBlock, LightStore, Result) {
auxLS.Init
// first we ask for the heights the ibc component is aware of
ibcHeights = ibc.QueryHeightsRange(
ibc.id,
lightStore.LowestVerified().Height,
lblock.Height - 1);
// this function does not exist yet. Alternatively, we may
// request all transactions that installed headers via CosmosSDK
for {
h, result = max(ibcHeights)
if result = Empty {
return (_, _, _, NoRoot)
}
ibcLightBlock = ibc.queryChainConsensusState(h)
auxLS.Update(ibcLightBlock, StateVerified);
connector, result := Connector(lightStore, ibcLightBlock, lblock.Header.Height)
if result = success {
return (ibcLightBlock, connector, auxLS, Success)
}
else{
ibcHeights.remove(h)
}
}
}
```
- Expected postcondition
- returns
- a lightBlock b1 from the IBC component, and
- a lightBlock b2
from the local lightStore with height less than
lblock.Header.Hight, s.t. b1 supports b2, and
- a lightstore with the blocks downloaded from
the ibc component
----
#### [TAG-LS-FUNC-CONNECT.1]
```go
func Connector (lightStore LightStore, lb LightBlock, h Height) (LightBlock, bool)
```
- Expected postcondition
- returns a verified LightBlock from lightStore with height less
than *h* that can be
verified by lb in one step.
**TODO:** for the above to work we need an invariant that all verified
lightblocks form a chain of trust. Otherwise, we need a lightblock
that has a chain of trust to height.
> Once the common root is found, a proof of fork that will be accepted
> by the IBC component needs to be generated. This is done in the
> following function.
#### [TAG-EXTEND-POF.1]
```go
func extendPoF (root LightBlock,
connector LightBlock,
lightStore LightStore,
Pof LightNodeProofofFork) (LightNodeProofofFork}
```
- Implementation remark
- PoF is not sufficient to convince an IBC component, so we extend
the proof of fork farther in the past
- Expected postcondition
- returns a newPOF:
- newPoF.TrustedBlock = root
- let prefix =
connector +
lightStore.Subtrace(connector.Header.Height, PoF.TrustedBlock.Header.Height-1) +
PoF.TrustedBlock
- newPoF.PrimaryTrace = prefix + PoF.PrimaryTrace
- newPoF.SecondaryTrace = prefix + PoF.SecondaryTrace
### Detection a fork at the IBC component
The following functions is assumed to be called regularly to check
that latest consensus state of the IBC component. Alternatively, this
logic can be executed whenever the relayer is informed (via an event)
that a new header has been installed.
#### [TAG-HANDLER-DETECT-FORK.1]
```go
func DetectIBCFork(ibc IBCComponent, lightStore LightStore) (LightNodeProofOfFork, Error) {
cs = ibc.queryClientState(ibc);
lb, found := lightStore.Get(cs.Header.Height)
if !found {
**TODO:** need verify to target
lb, result = LightClient.Main(primary, lightStore, cs.Header.Height)
// [LCV-FUNC-IBCMAIN.1]
**TODO** decide what to do following the outcome of Issue #499
// I guess here we have to get into the light client
}
if cs != lb {
// IBC component disagrees with my primary.
// I fetch the
ibcLightBlock, lblock, ibcStore, result := commonRoot(lightStore, ibc, lb)
pof = new LightNodeProofOfFork;
pof.TrustedBlock := ibcLightBlock
pof.PrimaryTrace := ibcStore + cs
pof.SecondaryTrace := lightStore.Subtrace(lblock.Header.Height,
lb.Header.Height);
return(pof, Fork)
}
return(nil , NoFork)
}
```
**TODO:** finish conditions
- Implementation remark
- we ask the handler for the lastest check. Cross-check with the
chain. In case they deviate we generate PoF.
- we assume IBC component is correct. It has verified the
consensus state
- Expected precondition
- Expected postcondition
@@ -0,0 +1,345 @@
# Requirements for Fork Detection in the IBC Context
## What you need to know about IBC
In the following, I distilled what I considered relevant from
<https://github.com/cosmos/ics/tree/master/spec/ics-002-client-semantics>
### Components and their interface
#### Tendermint Blockchains
> I assume you know what that is.
#### An IBC/Tendermint correspondence
| IBC Term | Tendermint-RS Spec Term | Comment |
|----------|-------------------------| --------|
| `CommitmentRoot` | AppState | app hash |
| `ConsensusState` | Lightblock | not all fields are there. NextValidator is definitly needed |
| `ClientState` | latest light block + configuration parameters (e.g., trusting period + `frozenHeight` | NextValidators missing; what is `proofSpecs`?|
| `frozenHeight` | height of fork | set when a fork is detected |
| "would-have-been-fooled" | light node fork detection | light node may submit proof of fork to IBC component to halt it |
| `Height` | (no epochs) | (epoch,height) pair in lexicographical order (`compare`) |
| `Header` | ~signed header | validatorSet explicit (no hash); nextValidators missing |
| `Evidence` | t.b.d. | definition unclear "which the light client would have considered valid". Data structure will need to change |
| `verify` | `ValidAndVerified` | signature does not match perfectly (ClientState vs. LightBlock) + in `checkMisbehaviourAndUpdateState` it is unclear whether it uses traces or goes to h1 and h2 in one step |
#### Some IBC links
- [QueryConsensusState](https://github.com/cosmos/cosmos-sdk/blob/2651427ab4c6ea9f81d26afa0211757fc76cf747/x/ibc/02-client/client/utils/utils.go#L68)
#### Required Changes in ICS 007
- `assert(height > 0)` in definition of `initialise` doesn't match
definition of `Height` as *(epoch,height)* pair.
- `initialise` needs to be updated to new data structures
- `clientState.frozenHeight` semantics seem not totally consistent in
document. E.g., `min` needs to be defined over optional value in
`checkMisbehaviourAndUpdateState`. Also, if you are frozen, why do
you accept more evidence.
- `checkValidityAndUpdateState`
- `verify`: it needs to be clarified that checkValidityAndUpdateState
does not perform "bisection" (as currently hinted in the text) but
performs a single step of "skipping verification", called,
`ValidAndVerified`
- `assert (header.height > clientState.latestHeight)`: no old
headers can be installed. This might be OK, but we need to check
interplay with misbehavior
- clienstState needs to be updated according to complete data
structure
- `checkMisbehaviourAndUpdateState`: as evidence will contain a trace
(or two), the assertion that uses verify will need to change.
- ICS 002 states w.r.t. `queryChainConsensusState` that "Note that
retrieval of past consensus states by height (as opposed to just the
current consensus state) is convenient but not required." For
Tendermint fork detection, this seems to be a necessity.
- `Header` should become a lightblock
- `Evidence` should become `LightNodeProofOfFork` [LCV-DATA-POF.1]
- `upgradeClientState` what is the semantics (in particular what is
`height` doing?).
- `checkMisbehaviourAndUpdateState(cs: ClientState, PoF:
LightNodeProofOfFork)` needs to be adapted
#### Handler
A blockchain runs a **handler** that passively collects information about
other blockchains. It can be thought of a state machine that takes
input events.
- the state includes a lightstore (I guess called `ConsensusState`
in IBC)
- The following function is used to pass a header to a handler
```go
type checkValidityAndUpdateState = (Header) => Void
```
For Tendermint, it will perform
`ValidandVerified`, that is, it does the trusting period check and the
+1/3 check (+2/3 for sequential headers).
If it verifies a header, it adds it to its lightstore,
if it does not pass verification it drops it.
Right now it only accepts a header more recent then the latest
header,
and drops older
ones or ones that could not be verified.
> The above paragraph captures what I believe what is the current
logic of `checkValidityAndUpdateState`. It may be subject to
change. E.g., maintain a lightstore with state (unverified, verified)
- The following function is used to pass "evidence" (this we
will need to make precise eventually) to a handler
```go
type checkMisbehaviourAndUpdateState = (bytes) => Void
```
We have to design this, and the data that the handler can use to
check that there was some misbehavior (fork) in order react on
it, e.g., flagging a situation and
stop the protocol.
- The following function is used to query the light store (`ConsensusState`)
```go
type queryChainConsensusState = (height: uint64) => ConsensusState
```
#### Relayer
- The active components are called **relayer**.
- a relayer contains light clients to two (or more?) blockchains
- the relayer send headers and data to the handler to invoke
`checkValidityAndUpdateState` and
`checkMisbehaviourAndUpdateState`. It may also query
`queryChainConsensusState`.
- multiple relayers may talk to one handler. Some relayers might be
faulty. We assume existence of at least single correct relayer.
## Informal Problem Statement: Fork detection in IBC
### Relayer requirement: Evidence for Handler
- The relayer should provide the handler with
"evidence" that there was a fork.
- The relayer can read the handler's consensus state. Thus the relayer can
feed the handler precisely the information the handler needs to detect a
fork.
What is this
information needs to be specified.
- The information depends on the verification the handler does. It
might be necessary to provide a bisection proof (list of
lightblocks) so that the handler can verify based on its local
lightstore a header *h* that is conflicting with a header *h'* in the
local lightstore, that is, *h != h'* and *h.Height = h'.Height*
### Relayer requirement: Fork detection
Let's assume there is a fork at chain A. There are two ways the
relayer can figure that out:
1. as the relayer contains a light client for A, it also includes a fork
detector that can detect a fork.
2. the relayer may also detect a fork by observing that the
handler for chain A (on chain B)
is on a different branch than the relayer
- in both detection scenarios, the relayer should submit evidence to
full nodes of chain A where there is a fork. As we assume a fullnode
has a complete list of blocks, it is sufficient to send "Bucky's
evidence" (<https://github.com/tendermint/tendermint/issues/5083>),
that is,
- two lightblocks from different branches +
- a lightblock (perhaps just a height) from which both blocks
can be verified.
- in the scenario 2., the relayer must feed the A-handler (on chain B)
a proof of a fork on A so that chain B can react accordingly
### Handler requirement
- there are potentially many relayers, some correct some faulty
- a handler cannot trust the information provided by the relayer,
but must verify
(Доверя́й, но проверя́й)
- in case of a fork, we accept that the handler temporarily stores
headers (tagged as verified).
- eventually, a handler should be informed
(`checkMisbehaviourAndUpdateState`)
by some relayer that it has
verified a header from a fork. Then the handler should do what is
required by IBC in this case (stop?)
### Challenges in the handler requirement
- handlers and relayers work on different lightstores. In principle
the lightstore need not intersect in any heights a priori
- if a relayer sees a header *h* it doesn't know at a handler (`queryChainConsensusState`), the
relayer needs to
verify that header. If it cannot do it locally based on downloaded
and verified (trusted?) light blocks, it might need to use
`VerifyToTarget` (bisection). To call `VerifyToTarget` we might keep
*h* in the lightstore. If verification fails, we need to download the
"alternative" header of height *h.Height* to generate evidence for
the handler.
- we have to specify what precisely `queryChainConsensusState`
returns. It cannot be the complete lightstore. Is the last header enough?
- we would like to assume that every now and then (smaller than the
trusting period) a correct relayer checks whether the handler is on a
different branch than the relayer.
And we would like that this is enough to achieve
the Handler requirement.
- here the correctness argument would be easy if a correct relayer is
based on a light client with a *trusted* state, that is, a light
client who never changes its opinion about trusted. Then if such a
correct relayer checks-in with a handler, it will detect a fork, and
act in time.
- if the light client does not provide this interface, in the case of
a fork, we need some assumption about a correct relayer being on a
different branch than the handler, and we need such a relayer to
check-in not too late. Also
what happens if the relayer's light client is forced to roll-back
its lightstore?
Does it have to re-check all handlers?
## On the interconnectedness of things
In the broader discussion of so-called "fork accountability" there are
several subproblems
- Fork detection
- Evidence creation and submission
- Isolating misbehaving nodes (and report them for punishment over abci)
### Fork detection
The preliminary specification ./detection.md formalizes the notion of
a fork. Roughly, a fork exists if there are two conflicting headers
for the same height, where both are supported by bonded full nodes
(that have been validators in the near past, that is, within the
trusting period). We distinguish between *fork on the chain* where two
conflicting blocks are signed by +2/3 of the validators of that
height, and a *light client fork* where one of the conflicting headers
is not signed by +2/3 of the current height, but by +1/3 of the
validators of some smaller height.
In principle everyone can detect a fork
- ./detection talks about the Tendermint light client with a focus on
light nodes. A relayer runs such light clients and may detect
forks in this way
- in IBC, a relayer can see that a handler is on a conflicting branch
- the relayer should feed the handler the necessary information so
that it can halt
- the relayer should report the fork to a full node
### Evidence creation and submission
- the information sent from the relayer to the handler could be called
evidence, but this is perhaps a bad idea because the information sent to a
full node can also be called evidence. But this evidence might still
not be enough as the full node might need to run the "fork
accountability" protocol to generate evidence in the form of
consensus messages. So perhaps we should
introduce different terms for:
- proof of fork for the handler (basically consisting of lightblocks)
- proof of fork for a full node (basically consisting of (fewer) lightblocks)
- proof of misbehavior (consensus messages)
### Isolating misbehaving nodes
- this is the job of a full node.
- might be subjective in the future: the protocol depends on what the
full node believes is the "correct" chain. Right now we postulate
that every full node is on the correct chain, that is, there is no
fork on the chain.
- The full node figures out which nodes are
- lunatic
- double signing
- amnesic; **using the challenge response protocol**
- We do not punish "phantom" validators
- currently we understand a phantom validator as a node that
- signs a block for a height in which it is not in the
validator set
- the node is not part of the +1/3 of previous validators that
are used to support the header. Whether we call a validator
phantom might be subjective and depend on the header we
check against. Their formalization actually seems not so
clear.
- they can only do something if there are +1/3 faulty validators
that are either lunatic, double signing, or amnesic.
- abci requires that we only report bonded validators. So if a
node is a "phantom", we would need the check whether the node is
bonded, which currently is expensive, as it requires checking
blocks from the last three weeks.
- in the future, with state sync, a correct node might be
convinced by faulty nodes that it is in the validator set. Then
it might appear to be "phantom" although it behaves correctly
## Next steps
> The following points are subject to my limited knowledge of the
> state of the work on IBC. Some/most of it might already exist and we
> will just need to bring everything together.
- "proof of fork for a full node" defines a clean interface between
fork detection and misbehavior isolation. So it should be produced
by protocols (light client, the relayer). So we should fix that
first.
- Given the problems of not having a light client architecture spec,
for the relayer we should start with this. E.g.
- the relayer runs light clients for two chains
- the relayer regularly queries consensus state of a handler
- the relayer needs to check the consensus state
- this involves local checks
- this involves calling the light client
- the relayer uses the light client to do IBC business (channels,
packets, connections, etc.)
- the relayer submits proof of fork to handlers and full nodes
> the list is definitely not complete. I think part of this
> (perhaps all) is
> covered by what Anca presented recently.
We will need to define what we expect from these components
- for the parts where the relayer talks to the handler, we need to fix
the interface, and what the handler does
- we write specs for these components.