mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-19 06:31:57 +00:00
spec: merge rust-spec (#252)
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
==================================================================================
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user