Update specification of light client algorithm to align with the codepull/7804/head
@ -1,319 +1,3 @@ | |||
# Fork accountability -- Problem statement and attacks | |||
# Fork Accountability - MOVED! | |||
## Problem Statement | |||
Tendermint consensus guarantees the following specifications for all heights: | |||
* agreement -- no two correct full nodes decide differently. | |||
* validity -- the decided block satisfies the predefined predicate *valid()*. | |||
* termination -- all correct full nodes eventually decide, | |||
if the | |||
faulty validators have at most 1/3 of voting power in the current validator set. In the case where this assumption | |||
does not hold, each of the specification may be violated. | |||
The agreement property says that for a given height, any two correct validators that decide on a block for that height decide on the same block. That the block was indeed generated by the blockchain, can be verified starting from a trusted (genesis) block, and checking that all subsequent blocks are properly signed. | |||
However, faulty nodes may forge blocks and try to convince users (lite clients) that the blocks had been correctly generated. In addition, Tendermint agreement might be violated in the case where more than 1/3 of the voting power belongs to faulty validators: Two correct validators decide on different blocks. The latter case motivates the term "fork": as Tendermint consensus also agrees on the next validator set, correct validators may have decided on disjoint next validator sets, and the chain branches into two or more partitions (possibly having faulty validators in common) and each branch continues to generate blocks independently of the other. | |||
We say that a fork is a case in which there are two commits for different blocks at the same height of the blockchain. The proplem is to ensure that in those cases we are able to detect faulty validators (and not mistakenly accuse correct validators), and incentivize therefore validators to behave according to the protocol specification. | |||
**Conceptual Limit.** In order to prove misbehavior of a node, we have to show that the behavior deviates from correct behavior with respect to a given algorithm. Thus, an algorithm that detects misbehavior of nodes executing some algorithm *A* must be defined with respect to algorithm *A*. In our case, *A* is Tendermint consensus (+ other protocols in the infrastructure; e.g.,full nodes and the Lite Client). If the consensus algorithm is changed/updated/optimized in the future, we have to check whether changes to the accountability algorithm are also required. All the discussions in this document are thus inherently specific to Tendermint consensus and the Lite Client specification. | |||
**Q:** Should we distinguish agreement for validators and full nodes for agreement? The case where all correct validators agree on a block, but a correct full node decides on a different block seems to be slightly less severe that the case where two correct validators decide on different blocks. Still, if a contaminated full node becomes validator that may be problematic later on. Also it is not clear how gossiping is impaired if a contaminated full node is on a different branch. | |||
*Remark.* In the case more than 1/3 of the voting power belongs to faulty validators, also validity and termination can be broken. Termination can be broken if faulty processes just do not send the messages that are needed to make progress. Due to asynchrony, this is not punishable, because faulty validators can always claim they never received the messages that would have forced them to send messages. | |||
## The Misbehavior of Faulty Validators | |||
Forks are the result of faulty validators deviating from the protocol. In principle several such deviations can be detected without a fork actually occurring: | |||
1. double proposal: A faulty proposer proposes two different values (blocks) for the same height and the same round in Tendermint consensus. | |||
2. double signing: Tendermint consensus forces correct validators to prevote and precommit for at most one value per round. In case a faulty validator sends multiple prevote and/or precommit messages for different values for the same height/round, this is a misbehavior. | |||
3. lunatic validator: Tendermint consensus forces correct validators to prevote and precommit only for values *v* that satisfy *valid(v)*. If faulty validators prevote and precommit for *v* although *valid(v)=false* this is misbehavior. | |||
*Remark.* In isolation, Point 3 is an attack on validity (rather than agreement). However, the prevotes and precommits can then also be used to forge blocks. | |||
1. amnesia: Tendermint consensus has a locking mechanism. If a validator has some value v locked, then it can only prevote/precommit for v or nil. Sending prevote/precomit message for a different value v' (that is not nil) while holding lock on value v is misbehavior. | |||
2. spurious messages: In Tendermint consensus most of the message send instructions are guarded by threshold guards, e.g., one needs to receive *2f + 1* prevote messages to send precommit. Faulty validators may send precommit without having received the prevote messages. | |||
Independently of a fork happening, punishing this behavior might be important to prevent forks altogether. This should keep attackers from misbehaving: if at most 1/3 of the voting power is faulty, this misbehavior is detectable but will not lead to a safety violation. Thus, unless they have more than 1/3 (or in some cases more than 2/3) of the voting power attackers have the incentive to not misbehave. If attackers control too much voting power, we have to deal with forks, as discussed in this document. | |||
## Two types of forks | |||
* Fork-Full. Two correct validators decide on different blocks for the same height. Since also the next validator sets are decided upon, the correct validators may be partitioned to participate in two distinct branches of the forked chain. | |||
As in this case we have two different blocks (both having the same right/no right to exist), a central system invariant (one block per height decided by correct validators) is violated. As full nodes are contaminated in this case, the contamination can spread also to lite clients. However, even without breaking this system invariant, lite clients can be subject to a fork: | |||
* Fork-Lite. All correct validators decide on the same block for height *h*, but faulty processes (validators or not), forge a different block for that height, in order to fool users (who use the lite client). | |||
# Attack scenarios | |||
## On-chain attacks | |||
### Equivocation (one round) | |||
There are several scenarios in which forks might happen. The first is double signing within a round. | |||
* F1. Equivocation: faulty validators sign multiple vote messages (prevote and/or precommit) for different values *during the same round r* at a given height h. | |||
### Flip-flopping | |||
Tendermint consensus implements a locking mechanism: If a correct validator *p* receives proposal for value v and *2f + 1* prevotes for a value *id(v)* in round *r*, it locks *v* and remembers *r*. In this case, *p* also sends a precommit message for *id(v)*, which later may serve as proof that *p* locked *v*. | |||
In subsequent rounds, *p* only sends prevote messages for a value it had previously locked. However, it is possible to change the locked value if in a future round *r' > r*, if the process receives proposal and *2f + 1* prevotes for a different value *v'*. In this case, *p* could send a prevote/precommit for *id(v')*. This algorithmic feature can be exploited in two ways: | |||
* F2. Faulty Flip-flopping (Amnesia): faulty validators precommit some value *id(v)* in round *r* (value *v* is locked in round *r*) and then prevote for different value *id(v')* in higher round *r' > r* without previously correctly unlocking value *v*. In this case faulty processes "forget" that they have locked value *v* and prevote some other value in the following rounds. | |||
Some correct validators might have decided on *v* in *r*, and other correct validators decide on *v'* in *r'*. Here we can have branching on the main chain (Fork-Full). | |||
* F3. Correct Flip-flopping (Back to the past): There are some precommit messages signed by (correct) validators for value *id(v)* in round *r*. Still, *v* is not decided upon, and all processes move on to the next round. Then correct validators (correctly) lock and decide a different value *v'* in some round *r' > r*. And the correct validators continue; there is no branching on the main chain. | |||
However, faulty validators may use the correct precommit messages from round *r* together with a posteriori generated faulty precommit messages for round *r* to forge a block for a value that was not decided on the main chain (Fork-Lite). | |||
## Off-chain attacks | |||
F1-F3 may contaminate the state of full nodes (and even validators). Contaminated (but otherwise correct) full nodes may thus communicate faulty blocks to lite clients. | |||
Similarly, without actually interfering with the main chain, we can have the following: | |||
* F4. Phantom validators: faulty validators vote (sign prevote and precommit messages) in heights in which they are not part of the validator sets (at the main chain). | |||
* F5. Lunatic validator: faulty validator that sign vote messages to support (arbitrary) application state that is different from the application state that resulted from valid state transitions. | |||
## Types of victims | |||
We consider three types of potential attack victims: | |||
- FN: full node | |||
- LCS: lite client with sequential header verification | |||
- LCB: lite client with bisection based header verification | |||
F1 and F2 can be used by faulty validators to actually create multiple branches on the blockchain. That means that correctly operating full nodes decide on different blocks for the same height. Until a fork is detected locally by a full node (by receiving evidence from others or by some other local check that fails), the full node can spread corrupted blocks to lite clients. | |||
*Remark.* If full nodes take a branch different from the one taken by the validators, it may be that the liveness of the gossip protocol may be affected. We should eventually look at this more closely. However, as it does not influence safety it is not a primary concern. | |||
F3 is similar to F1, except that no two correct validators decide on different blocks. It may still be the case that full nodes become affected. | |||
In addition, without creating a fork on the main chain, lite clients can be contaminated by more than a third of validators that are faulty and sign a forged header | |||
F4 cannot fool correct full nodes as they know the current validator set. Similarly, LCS know who the validators are. Hence, F4 is an attack against LCB that do not necessarily know the complete prefix of headers (Fork-Lite), as they trust a header that is signed by at least one correct validator (trusting period method). | |||
The following table gives an overview of how the different attacks may affect different nodes. F1-F3 are *on-chain* attacks so they can corrupt the state of full nodes. Then if a lite client (LCS or LCB) contacts a full node to obtain headers (or blocks), the corrupted state may propagate to the lite client. | |||
F4 and F5 are *off-chain*, that is, these attacks cannot be used to corrupt the state of full nodes (which have sufficient knowledge on the state of the chain to not be fooled). | |||
| Attack | FN | LCS | LCB | | |||
|:------:|:------:|:------:|:------:| | |||
| F1 | direct | FN | FN | | |||
| F2 | direct | FN | FN | | |||
| F3 | direct | FN | FN | | |||
| F4 | | | direct | | |||
| F5 | | | direct | | |||
**Q:** Lite clients are more vulnerable than full nodes, because the former do only verify headers but do not execute transactions. What kind of certainty is gained by a full node that executes a transaction? | |||
As a full node verifies all transactions, it can only be | |||
contaminated by an attack if the blockchain itself violates its invariant (one block per height), that is, in case of a fork that leads to branching. | |||
## Detailed Attack Scenarios | |||
### Equivocation based attacks | |||
In case of equivocation based attacks, faulty validators sign multiple votes (prevote and/or precommit) in the same | |||
round of some height. This attack can be executed on both full nodes and lite clients. It requires more than 1/3 of voting power to be executed. | |||
#### Scenario 1: Equivocation on the main chain | |||
Validators: | |||
* CA - a set of correct validators with less than 1/3 of the voting power | |||
* CB - a set of correct validators with less than 1/3 of the voting power | |||
* CA and CB are disjoint | |||
* F - a set of faulty validators with more than 1/3 voting power | |||
Observe that this setting violates the Tendermint failure model. | |||
Execution: | |||
* A faulty proposer proposes block A to CA | |||
* A faulty proposer proposes block B to CB | |||
* Validators from the set CA and CB prevote for A and B, respectively. | |||
* Faulty validators from the set F prevote both for A and B. | |||
* The faulty prevote messages | |||
- for A arrive at CA long before the B messages | |||
- for B arrive at CB long before the A messages | |||
* Therefore correct validators from set CA and CB will observe | |||
more than 2/3 of prevotes for A and B and precommit for A and B, respectively. | |||
* Faulty validators from the set F precommit both values A and B. | |||
* Thus, we have more than 2/3 commits for both A and B. | |||
Consequences: | |||
* Creating evidence of misbehavior is simple in this case as we have multiple messages signed by the same faulty processes for different values in the same round. | |||
* We have to ensure that these different messages reach a correct process (full node, monitor?), which can submit evidence. | |||
* This is an attack on the full node level (Fork-Full). | |||
* It extends also to the lite clients, | |||
* For both we need a detection and recovery mechanism. | |||
#### Scenario 2: Equivocation to a lite client (LCS) | |||
Validators: | |||
* a set F of faulty validators with more than 2/3 of the voting power. | |||
Execution: | |||
* for the main chain F behaves nicely | |||
* F coordinates to sign a block B that is different from the one on the main chain. | |||
* the lite clients obtains B and trusts at as it is signed by more than 2/3 of the voting power. | |||
Consequences: | |||
Once equivocation is used to attack lite client it opens space | |||
for different kind of attacks as application state can be diverged in any direction. For example, it can modify validator set such that it contains only validators that do not have any stake bonded. Note that after a lite client is fooled by a fork, that means that an attacker can change application state and validator set arbitrarily. | |||
In order to detect such (equivocation-based attack), the lite client would need to cross check its state with some correct validator (or to obtain a hash of the state from the main chain using out of band channels). | |||
*Remark.* The lite client would be able to create evidence of misbehavior, but this would require to pull potentially a lot of data from correct full nodes. Maybe we need to figure out different architecture where a lite client that is attacked will push all its data for the current unbonding period to a correct node that will inspect this data and submit corresponding evidence. There are also architectures that assumes a special role (sometimes called fisherman) whose goal is to collect as much as possible useful data from the network, to do analysis and create evidence transactions. That functionality is outside the scope of this document. | |||
*Remark.* The difference between LCS and LCB might only be in the amount of voting power needed to convince lite client about arbitrary state. In case of LCB where security threshold is at minimum, an attacker can arbitrarily modify application state with more than 1/3 of voting power, while in case of LCS it requires more than 2/3 of the voting power. | |||
### Flip-flopping: Amnesia based attacks | |||
In case of amnesia, faulty validators lock some value *v* in some round *r*, and then vote for different value *v'* in higher rounds without correctly unlocking value *v*. This attack can be used both on full nodes and lite clients. | |||
#### Scenario 3: At most 2/3 of faults | |||
Validators: | |||
* a set F of faulty validators with more than 1/3 but at most 2/3 of the voting power | |||
* a set C of correct validators | |||
Execution: | |||
* Faulty validators commit (without exposing it on the main chain) a block A in round *r* by collecting more than 2/3 of the | |||
voting power (containing correct and faulty validators). | |||
* All validators (correct and faulty) reach a round *r' > r*. | |||
* Some correct validators in C do not lock any value before round *r'*. | |||
* The faulty validators in F deviate from Tendermint consensus by ignoring that they locked A in *r*, and propose a different block B in *r'*. | |||
* As the validators in C that have not locked any value find B acceptable, they accept the proposal for B and commit a block B. | |||
*Remark.* In this case, the more than 1/3 of faulty validators do not need to commit an equivocation (F1) as they only vote once per round in the execution. | |||
Detecting faulty validators in the case of such an attack can be done by the fork accountability mechanism described in: https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit?usp=sharing. | |||
If a lite client is attacked using this attack with more than 1/3 of voting power (and less than 2/3), the attacker cannot change the application state arbitrarily. Rather, the attacker is limited to a state a correct validator finds acceptable: In the execution above, correct validators still find the value acceptable, however, the block the lite client trusts deviates from the one on the main chain. | |||
#### Scenario 4: More than 2/3 of faults | |||
In case there is an attack with more than 2/3 of the voting power, an attacker can arbitrarily change application state. | |||
Validators: | |||
* a set F1 of faulty validators with more than 1/3 of the voting power | |||
* a set F2 of faulty validators with at most 1/3 of the voting power | |||
Execution | |||
* Similar to Scenario 3 (however, messages by correct validators are not needed) | |||
* The faulty validators in F1 lock value A in round *r* | |||
* They sign a different value in follow-up rounds | |||
* F2 does not lock A in round *r* | |||
Consequences: | |||
* The validators in F1 will be detectable by the the fork accountability mechanisms. | |||
* The validators in F2 cannot be detected using this mechanism. | |||
Only in case they signed something which conflicts with the application this can be used against them. Otherwise they do not do anything incorrect. | |||
* This case is not covered by the report https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit?usp=sharing as it only assumes at most 2/3 of faulty validators. | |||
**Q:** do we need to define a special kind of attack for the case where a validator sign arbitrarily state? It seems that detecting such attack requires a different mechanism that would require as an evidence a sequence of blocks that led to that state. This might be very tricky to implement. | |||
### Back to the past | |||
In this kind of attack, faulty validators take advantage of the fact that they did not sign messages in some of the past rounds. Due to the asynchronous network in which Tendermint operates, we cannot easily differentiate between such an attack and delayed message. This kind of attack can be used at both full nodes and lite clients. | |||
#### Scenario 5: | |||
Validators: | |||
* C1 - a set of correct validators with 1/3 of the voting power | |||
* C2 - a set of correct validators with 1/3 of the voting power | |||
* C1 and C2 are disjoint | |||
* F - a set of faulty validators with 1/3 voting power | |||
* one additional faulty process *q* | |||
* F and *q* violate the Tendermint failure model. | |||
Execution: | |||
* in a round *r* of height *h* we have C1 precommitting a value A, | |||
* C2 precommits nil, | |||
* F does not send any message | |||
* *q* precommits nil. | |||
* In some round *r' > r*, F and *q* and C2 commit some other value B different from A. | |||
* F and *fp* "go back to the past" and sign precommit message for value A in round *r*. | |||
* Together with precomit messages of C1 this is sufficient for a commit for value A. | |||
Consequences: | |||
* Only a single faulty validator that previously precommited nil did equivocation, while the other 1/3 of faulty validators actually executed an attack that has exactly the same sequence of messages as part of amnesia attack. Detecting this kind of attack boil down to mechanisms for equivocation and amnesia. | |||
**Q:** should we keep this as a separate kind of attack? It seems that equivocation, amnesia and phantom validators are the only kind of attack we need to support and this gives us security also in other cases. This would not be surprising as equivocation and amnesia are attacks that followed from the protocol and phantom attack is not really an attack to Tendermint but more to the Proof of Stake module. | |||
### Phantom validators | |||
In case of phantom validators, processes that are not part of the current validator set but are still bonded (as attack happen during their unbonding period) can be part of the attack by signing vote messages. This attack can be executed against both full nodes and lite clients. | |||
#### Scenario 6: | |||
Validators: | |||
* F -- a set of faulty validators that are not part of the validator set on the main chain at height *h + k* | |||
Execution: | |||
* There is a fork, and there exist two different headers for height *h + k*, with different validator sets: | |||
- VS2 on the main chain | |||
- forged header VS2', signed by F (and others) | |||
* a lite client has a trust in a header for height *h* (and the corresponding validator set VS1). | |||
* As part of bisection header verification, it verifies the header at height *h + k* with new validator set VS2'. | |||
Consequences: | |||
* To detect this, a node needs to see both, the forged header and the canonical header from the chain. | |||
* If this is the case, detecting these kind of attacks is easy as it just requires verifying if processes are signing messages in heights in which they are not part of the validator set. | |||
**Remark.** We can have phantom-validator-based attacks as a follow up of equivocation or amnesia based attack where forked state contains validators that are not part of the validator set at the main chain. In this case, they keep signing messages contributed to a forked chain (the wrong branch) although they are not part of the validator set on the main chain. This attack can also be used to attack full node during a period of time it is eclipsed. | |||
### Lunatic validator | |||
Lunatic validator agrees to sign commit messages for arbitrary application state. It is used to attack lite clients. | |||
Note that detecting this behavior require application knowledge. Detecting this behavior can probably be done by | |||
referring to the block before the one in which height happen. | |||
**Q:** can we say that in this case a validator declines to check if a proposed value is valid before voting for it? | |||
Fork Accountability has moved to [light-client](./light-client/accountability). |
@ -1,318 +1,3 @@ | |||
# Lite client | |||
# Light Client - MOVED! | |||
A lite client is a process that connects to Tendermint full nodes and then tries to verify application data using the Merkle proofs. | |||
## Context of this document | |||
In order to make sure that full nodes have the incentive to follow the protocol, we have to address the following three Issues | |||
1. The lite client needs a method to verify headers it obtains from full nodes according to trust assumptions -- this document. | |||
2. The lite client must be able to connect to one correct full node to detect and report on failures in the trust assumptions (i.e., conflicting headers) -- a future document. | |||
3. In the event the trust assumption fails (i.e., a lite client is fooled by a conflicting header), the Tendermint fork accountability protocol must account for the evidence -- see #3840 | |||
## Problem statement | |||
We assume that the lite client knows a (base) header _inithead_ it trusts (by social consensus or because the lite client has decided to trust the header before). The goal is to check whether another header _newhead_ can be trusted based on the data in _inithead_. | |||
The correctness of the protocol is based on the assumption that _inithead_ was generated by an instance of Tendermint consensus. The term "trusting" above indicates that the correctness on the protocol depends on this assumption. It is in the responsibility of the user that runs the lite client to make sure that the risk of trusting a corrupted/forged _inithead_ is negligible. | |||
## Definitions | |||
### Data structures | |||
In the following, only the details of the data structures needed for this specification are given. | |||
- header fields | |||
- _height_ | |||
- _bfttime_: the chain time when the header (block) was generated | |||
- _V_: validator set containing validators for this block. | |||
- _NextV_: validator set for next block. | |||
- _commit_: evidence that block with height _height_ - 1 was committed by a set of validators (canonical commit). We will use `signers(commit)` to refer to the set of validators that committed the block. | |||
- signed header fields: contains a header and a _commit_ for the current header; a "seen commit". In the Tendermint consensus the "canonical commit" is stored in header _height_ + 1. | |||
- For each header _h_ it has locally stored, the lite client stores whether | |||
it trusts _h_. We write _trust(h) = true_, if this is the case. | |||
- Validator fields. We will write a validator as a tuple _(v,p)_ such that | |||
- _v_ is the identifier (we assume identifiers are unique in each validator set) | |||
- _p_ is its voting powers | |||
### Functions | |||
For the purpose of this lite client specification, we assume that the Tendermint Full Node exposes the following function over Tendermint RPC: | |||
```go | |||
func Commit(height int64) (SignedHeader, error) | |||
// returns signed header: header (with the fields from | |||
// above) with Commit that include signatures of | |||
// validators that signed the header | |||
type SignedHeader struct { | |||
Header Header | |||
Commit Commit | |||
} | |||
``` | |||
### Definitions | |||
- _tp_: trusting period | |||
- for realtime _t_, the predicate _correct(v,t)_ is true if the validator _v_ | |||
follows the protocol until time _t_ (we will see about recovery later). | |||
### Tendermint Failure Model | |||
If a block _h_ is generated at time _bfttime_ (and this time is stored in the block), then a set of validators that hold more than 2/3 of the voting power in h.Header.NextV is correct until time h.Header.bfttime + tp. | |||
Formally, | |||
\[ | |||
\sum*{(v,p) \in h.Header.NextV \wedge correct(v,h.Header.bfttime + tp)} p > | |||
2/3 \sum*{(v,p) \in h.Header.NextV} p | |||
\] | |||
_Assumption_: "correct" is defined w.r.t. realtime (some Newtonian global notion of time, i.e., wall time), while _bfttime_ corresponds to the reading of the local clock of a validator (how this time is computed may change when the Tendermint consensus is modified). In this note, we assume that all clocks are synchronized to realtime. We can make this more precise eventually (incorporating clock drift, accuracy, precision, etc.). Right now, we consider this assumption sufficient, as clock synchronization (under NTP) is in the order of milliseconds and _tp_ is in the order of weeks. | |||
_Remark_: This failure model might change to a hybrid version that takes heights into account in the future. | |||
The specification in this document considers an implementation of the lite client under this assumption. Issues like _counter-factual signing_ and _fork accountability_ and _evidence submission_ are mechanisms that justify this assumption by incentivizing validators to follow the protocol. | |||
If they don't, and we have more that 1/3 faults, safety may be violated. Our approach then is to _detect_ these cases (after the fact), and take suitable repair actions (automatic and social). This is discussed in an upcoming document on "Fork accountability". (These safety violations include the lite client wrongly trusting a header, a fork in the blockchain, etc.) | |||
## Lite Client Trusting Spec | |||
The lite client communicates with a full node and learns new headers. The goal is to locally decide whether to trust a header. Our implementation needs to ensure the following two properties: | |||
- Lite Client Completeness: If header _h_ was correctly generated by an instance of Tendermint consensus (and its age is less than the trusting period), then the lite client should eventually set _trust(h)_ to true. | |||
- Lite Client Accuracy: If header _h_ was _not generated_ by an instance of Tendermint consensus, then the lite client should never set _trust(h)_ to true. | |||
_Remark_: If in the course of the computation, the lite client obtains certainty that some headers were forged by adversaries (that is were not generated by an instance of Tendermint consensus), it may submit (a subset of) the headers it has seen as evidence of misbehavior. | |||
_Remark_: In Completeness we use "eventually", while in practice _trust(h)_ should be set to true before _h.Header.bfttime + tp_. If not, the block cannot be trusted because it is too old. | |||
_Remark_: If a header _h_ is marked with _trust(h)_, but it is too old (its bfttime is more than _tp_ ago), then the lite client should set _trust(h)_ to false again. | |||
_Assumption_: Initially, the lite client has a header _inithead_ that it trusts correctly, that is, _inithead_ was correctly generated by the Tendermint consensus. | |||
To reason about the correctness, we may prove the following invariant. | |||
_Verification Condition: Lite Client Invariant._ | |||
For each lite client _l_ and each header _h_: | |||
if _l_ has set _trust(h) = true_, | |||
then validators that are correct until time _h.Header.bfttime + tp_ have more than two thirds of the voting power in _h.Header.NextV_. | |||
Formally, | |||
\[ | |||
\sum*{(v,p) \in h.Header.NextV \wedge correct(v,h.Header.bfttime + tp)} p > | |||
2/3 \sum*{(v,p) \in h.Header.NextV} p | |||
\] | |||
_Remark._ To prove the invariant, we will have to prove that the lite client only trusts headers that were correctly generated by Tendermint consensus, then the formula above follows from the Tendermint failure model. | |||
## High Level Solution | |||
Upon initialization, the lite client is given a header _inithead_ it trusts (by | |||
social consensus). It is assumed that _inithead_ satisfies the lite client invariant. (If _inithead_ has been correctly generated by Tendermint consensus, the invariant follows from the Tendermint Failure Model.) | |||
When a lite clients sees a signed new header _snh_, it has to decide whether to trust the new | |||
header. Trust can be obtained by (possibly) the combination of three methods. | |||
1. **Uninterrupted sequence of proof.** If a block is appended to the chain, where the last block | |||
is trusted (and properly committed by the old validator set in the next block), | |||
and the new block contains a new validator set, the new block is trusted if the lite client knows all headers in the prefix. | |||
Intuitively, a trusted validator set is assumed to only chose a new validator set that will obey the Tendermint Failure Model. | |||
2. **Trusting period.** Based on a trusted block _h_, and the lite client | |||
invariant, which ensures the fault assumption during the trusting period, we can check whether at least one validator, that has been continuously correct from _h.Header.bfttime_ until now, has signed _snh_. | |||
If this is the case, similarly to above, the chosen validator set in _snh_ does not violate the Tendermint Failure Model. | |||
3. **Bisection.** If a check according to the trusting period fails, the lite client can try to obtain a header _hp_ whose height lies between _h_ and _snh_ in order to check whether _h_ can be used to get trust for _hp_, and _hp_ can be used to get trust for _snh_. If this is the case we can trust _snh_; if not, we may continue recursively. | |||
## How to use it | |||
We consider the following use case: | |||
the lite client wants to verify a header for some given height _k_. Thus: | |||
- it requests the signed header for height _k_ from a full node | |||
- it tries to verify this header with the methods described here. | |||
This can be used in several settings: | |||
- someone tells the lite client that application data that is relevant for it can be read in the block of height _k_. | |||
- the lite clients wants the latest state. It asks a full node for the current height, and uses the response for _k_. | |||
## Details | |||
_Assumptions_ | |||
1. _tp < unbonding period_. | |||
2. _snh.Header.bfttime < now_ | |||
3. _snh.Header.bfttime < h.Header.bfttime+tp_ | |||
4. _trust(h)=true_ | |||
**Observation 1.** If _h.Header.bfttime + tp > now_, we trust the old | |||
validator set _h.Header.NextV_. | |||
When we say we trust _h.Header.NextV_ we do _not_ trust that each individual validator in _h.Header.NextV_ is correct, but we only trust the fact that at most 1/3 of them are faulty (more precisely, the faulty ones have at most 1/3 of the total voting power). | |||
### Functions | |||
The function _Bisection_ checks whether to trust header _h2_ based on the trusted header _h1_. It does so by calling | |||
the function _CheckSupport_ in the process of | |||
bisection/recursion. _CheckSupport_ implements the trusted period method and, for two adjacent headers (in term of heights), it checks uninterrupted sequence of proof. | |||
_Assumption_: In the following, we assume that _h2.Header.height > h1.Header.height_. We will quickly discuss the other case in the next section. | |||
We consider the following set-up: | |||
- the lite client communicates with one full node | |||
- the lite client locally stores all the signed headers it obtained (trusted or not). In the pseudo code below we write _Store(header)_ for this. | |||
- If _Bisection_ returns _false_, then the lite client has seen a forged header. | |||
- However, it does not know which header(s) is/are the problematic one(s). | |||
- In this case, the lite client can submit (some of) the headers it has seen as evidence. As the lite client communicates with one full node only when executing Bisection, there are two cases | |||
- the full node is faulty | |||
- the full node is correct and there was a fork in Tendermint consensus. Header _h1_ is from a different branch than the one taken by the full node. This case is not focus of this document, but will be treated in the document on fork accountability. | |||
- the lite client must retry to retrieve correct headers from another full node | |||
- it picks a new full node | |||
- it restarts _Bisection_ | |||
- there might be optimizations; a lite client may not need to call _Commit(k)_, for a height _k_ for which it already has a signed header it trusts. | |||
- how to make sure that a lite client can communicate with a correct full node will be the focus of a separate document (recall Issue 3 from "Context of this document"). | |||
**Auxiliary Functions.** We will use the function `votingpower_in(V1,V2)` to compute the voting power the validators in set V1 have according to their voting power in set V2; | |||
we will write `totalVotingPower(V)` for `votingpower_in(V,V)`, which returns the total voting power in V. | |||
We further use the function `signers(Commit)` that returns the set of validators that signed the Commit. | |||
**CheckSupport.** The following function checks whether we can trust the header h2 based on header h1 following the trusting period method. | |||
```go | |||
func CheckSupport(h1,h2,trustlevel) bool { | |||
if h1.Header.bfttime + tp < now { // Observation 1 | |||
return false // old header was once trusted but it is expired | |||
} | |||
vp_all := totalVotingPower(h1.Header.NextV) | |||
// total sum of voting power of validators in h2 | |||
if h2.Header.height == h1.Header.height + 1 { | |||
// specific check for adjacent headers; everything must be | |||
// properly signed. | |||
// also check that h2.Header.V == h1.Header.NextV | |||
// Plus the following check that 2/3 of the voting power | |||
// in h1 signed h2 | |||
return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > | |||
2/3 * vp_all) | |||
// signing validators are more than two third in h1. | |||
} | |||
return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > | |||
max(1/3,trustlevel) * vp_all) | |||
// get validators in h1 that signed h2 | |||
// sum of voting powers in h1 of | |||
// validators that signed h2 | |||
// is more than a third in h1 | |||
} | |||
``` | |||
_Remark_: Basic header verification must be done for _h2_. Similar checks are done in: | |||
https://github.com/tendermint/tendermint/blob/master/types/validator_set.go#L591-L633 | |||
_Remark_: There are some sanity checks which are not in the code: | |||
_h2.Header.height > h1.Header.height_ and _h2.Header.bfttime > h1.Header.bfttime_ and _h2.Header.bfttime < now_. | |||
_Remark_: `return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > max(1/3,trustlevel) * vp_all)` may return false even if _h2_ was properly generated by Tendermint consensus in the case of big changes in the validator sets. However, the check `return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > 2/3 * vp_all)` must return true if _h1_ and _h2_ were generated by Tendermint consensus. | |||
_Remark_: The 1/3 check differs from a previously proposed method that was based on intersecting validator sets and checking that the new validator set contains "enough" correct validators. We found that the old check is not suited for realistic changes in the validator sets. The new method is not only based on cardinalities, but also exploits that we can trust what is signed by a correct validator (i.e., signed by more than 1/3 of the voting power). | |||
_Correctness arguments_ | |||
Towards Lite Client Accuracy: | |||
- Assume by contradiction that _h2_ was not generated correctly and the lite client sets trust to true because _CheckSupport_ returns true. | |||
- h1 is trusted and sufficiently new | |||
- by Tendermint Fault Model, less than 1/3 of voting power held by faulty validators => at least one correct validator _v_ has signed _h2_. | |||
- as _v_ is correct up to now, it followed the Tendermint consensus protocol at least up to signing _h2_ => _h2_ was correctly generated, we arrive at the required contradiction. | |||
Towards Lite Client Completeness: | |||
- The check is successful if sufficiently many validators of _h1_ are still validators in _h2_ and signed _h2_. | |||
- If _h2.Header.height = h1.Header.height + 1_, and both headers were generated correctly, the test passes | |||
_Verification Condition:_ We may need a Tendermint invariant stating that if _h2.Header.height = h1.Header.height + 1_ then _signers(h2.Commit) \subseteq h1.Header.NextV_. | |||
_Remark_: The variable _trustlevel_ can be used if the user believes that relying on one correct validator is not sufficient. However, in case of (frequent) changes in the validator set, the higher the _trustlevel_ is chosen, the more unlikely it becomes that CheckSupport returns true for non-adjacent headers. | |||
**Bisection.** The following function uses CheckSupport in a recursion to find intermediate headers that allow to establish a sequence of trust. | |||
```go | |||
func Bisection(h1,h2,trustlevel) bool{ | |||
if CheckSupport(h1,h2,trustlevel) { | |||
return true | |||
} | |||
if h2.Header.height == h1.Header.height + 1 { | |||
// we have adjacent headers that are not matching (failed | |||
// the CheckSupport) | |||
// we could submit evidence here | |||
return false | |||
} | |||
pivot := (h1.Header.height + h2.Header.height) / 2 | |||
hp := Commit(pivot) | |||
// ask a full node for header of height pivot | |||
Store(hp) | |||
// store header hp locally | |||
if Bisection(h1,hp,trustlevel) { | |||
// only check right branch if hp is trusted | |||
// (otherwise a lot of unnecessary computation may be done) | |||
return Bisection(hp,h2,trustlevel) | |||
} | |||
else { | |||
return false | |||
} | |||
} | |||
``` | |||
_Correctness arguments (sketch)_ | |||
Lite Client Accuracy: | |||
- Assume by contradiction that _h2_ was not generated correctly and the lite client sets trust to true because Bisection returns true. | |||
- Bisection returns true only if all calls to CheckSupport in the recursion return true. | |||
- Thus we have a sequence of headers that all satisfied the CheckSupport | |||
- again a contradiction | |||
Lite Client Completeness: | |||
This is only ensured if upon _Commit(pivot)_ the lite client is always provided with a correctly generated header. | |||
_Stalling_ | |||
With Bisection, a faulty full node could stall a lite client by creating a long sequence of headers that are queried one-by-one by the lite client and look OK, before the lite client eventually detects a problem. There are several ways to address this: | |||
- Each call to `Commit` could be issued to a different full node | |||
- Instead of querying header by header, the lite client tells a full node which header it trusts, and the height of the header it needs. The full node responds with the header along with a proof consisting of intermediate headers that the light client can use to verify. Roughly, Bisection would then be executed at the full node. | |||
- We may set a timeout how long bisection may take. | |||
### The case _h2.Header.height < h1.Header.height_ | |||
In the use case where someone tells the lite client that application data that is relevant for it can be read in the block of height _k_ and the lite client trusts a more recent header, we can use the hashes to verify headers "down the chain." That is, we iterate down the heights and check the hashes in each step. | |||
_Remark._ For the case were the lite client trusts two headers _i_ and _j_ with _i < k < j_, we should discuss/experiment whether the forward or the backward method is more effective. | |||
```go | |||
func Backwards(h1,h2) bool { | |||
assert (h2.Header.height < h1.Header.height) | |||
old := h1 | |||
for i := h1.Header.height - 1; i > h2.Header.height; i-- { | |||
new := Commit(i) | |||
Store(new) | |||
if (hash(new) != old.Header.hash) { | |||
return false | |||
} | |||
old := new | |||
} | |||
return (hash(h2) == old.Header.hash) | |||
} | |||
``` | |||
Light Client has moved to [light-client](./light-client). |
@ -0,0 +1,66 @@ | |||
# Tendermint Light Client Protocol | |||
NOTE: This specification is under heavy development and is not yet complete nor | |||
accurate. | |||
## Contents | |||
- [Motivation](#motivation) | |||
- [Structure](#structure) | |||
- [Core Verification](./verification.md) | |||
- [Fork Detection](./detection.md) | |||
- [Fork Accountability](./accountability.md) | |||
## Motivation | |||
The Tendermint Light Client is motivated by the need for a light weight protocol | |||
to sync with a Tendermint blockchain, with the least processing necessary to | |||
securely verify a recent state. The protocol consists of managing trusted validator | |||
sets and trusted block headers, and is based primarily on checking hashes | |||
and verifying Tendermint commit signatures. | |||
Motivating use cases include: | |||
- Light Node: a daemon that syncs a blockchain to the latest committed header by making RPC requests to full nodes. | |||
- State Sync: a reactor that syncs a blockchain to a recent committed state by making P2P requests to full nodes. | |||
- IBC Client: an ABCI application library that syncs a blockchain to a recent committed header by receiving proof-carrying | |||
transactions from "IBC relayers", who make RPC requests to full nodes on behalf of the IBC clients. | |||
## Structure | |||
### Components | |||
The Tendermint Light Client consists of three primary components: | |||
- [Core Verification](./verification.md): verifying hashes, signatures, and validator set changes | |||
- [Fork Detection](./detection.md): talking to multiple peers to detect Byzantine behaviour | |||
- [Fork Accountability](./accountability.md): analyzing Byzantine behaviour to hold validators accountable. | |||
While every light client must perform core verification and fork detection | |||
to achieve their prescribed security level, fork accountability is expected to | |||
be done by full nodes and validators, and is thus more accurately a component of | |||
the full node consensus protocol, though it is included here since it is | |||
primarily concerned with providing security to light clients. | |||
A schematic of the core verification and fork detection components in | |||
a Light Node are depicted below. The schematic is quite similar for other use cases. | |||
Note that fork accountability is not depicted, as it is the responsibility of the | |||
full nodes. | |||
![Light Client Diagram](./assets/light-node-image.png). | |||
### Synchrony | |||
Light clients are fundamentally synchronous protocols, | |||
where security is restricted by the interval during which a validator can be punished | |||
for Byzantine behaviour. We assume here that such intervals have fixed and known minimal duration | |||
referred to commonly as a blockchain's Unbonding Period. | |||
A secure light client must guarantee that all three components - | |||
core verification, fork detection, and fork accountability - | |||
each with their own synchrony assumptions and fault model, can execute | |||
sequentially and to completion within the given Unbonding Period. | |||
TODO: define all the synchrony parameters used in the protocol and their | |||
relation to the Unbonding Period. | |||
@ -0,0 +1,319 @@ | |||
# Fork accountability | |||
## Problem Statement | |||
Tendermint consensus guarantees the following specifications for all heights: | |||
* agreement -- no two correct full nodes decide differently. | |||
* validity -- the decided block satisfies the predefined predicate *valid()*. | |||
* termination -- all correct full nodes eventually decide, | |||
if the | |||
faulty validators have at most 1/3 of voting power in the current validator set. In the case where this assumption | |||
does not hold, each of the specification may be violated. | |||
The agreement property says that for a given height, any two correct validators that decide on a block for that height decide on the same block. That the block was indeed generated by the blockchain, can be verified starting from a trusted (genesis) block, and checking that all subsequent blocks are properly signed. | |||
However, faulty nodes may forge blocks and try to convince users (lite clients) that the blocks had been correctly generated. In addition, Tendermint agreement might be violated in the case where more than 1/3 of the voting power belongs to faulty validators: Two correct validators decide on different blocks. The latter case motivates the term "fork": as Tendermint consensus also agrees on the next validator set, correct validators may have decided on disjoint next validator sets, and the chain branches into two or more partitions (possibly having faulty validators in common) and each branch continues to generate blocks independently of the other. | |||
We say that a fork is a case in which there are two commits for different blocks at the same height of the blockchain. The proplem is to ensure that in those cases we are able to detect faulty validators (and not mistakenly accuse correct validators), and incentivize therefore validators to behave according to the protocol specification. | |||
**Conceptual Limit.** In order to prove misbehavior of a node, we have to show that the behavior deviates from correct behavior with respect to a given algorithm. Thus, an algorithm that detects misbehavior of nodes executing some algorithm *A* must be defined with respect to algorithm *A*. In our case, *A* is Tendermint consensus (+ other protocols in the infrastructure; e.g.,full nodes and the Lite Client). If the consensus algorithm is changed/updated/optimized in the future, we have to check whether changes to the accountability algorithm are also required. All the discussions in this document are thus inherently specific to Tendermint consensus and the Lite Client specification. | |||
**Q:** Should we distinguish agreement for validators and full nodes for agreement? The case where all correct validators agree on a block, but a correct full node decides on a different block seems to be slightly less severe that the case where two correct validators decide on different blocks. Still, if a contaminated full node becomes validator that may be problematic later on. Also it is not clear how gossiping is impaired if a contaminated full node is on a different branch. | |||
*Remark.* In the case more than 1/3 of the voting power belongs to faulty validators, also validity and termination can be broken. Termination can be broken if faulty processes just do not send the messages that are needed to make progress. Due to asynchrony, this is not punishable, because faulty validators can always claim they never received the messages that would have forced them to send messages. | |||
## The Misbehavior of Faulty Validators | |||
Forks are the result of faulty validators deviating from the protocol. In principle several such deviations can be detected without a fork actually occurring: | |||
1. double proposal: A faulty proposer proposes two different values (blocks) for the same height and the same round in Tendermint consensus. | |||
2. double signing: Tendermint consensus forces correct validators to prevote and precommit for at most one value per round. In case a faulty validator sends multiple prevote and/or precommit messages for different values for the same height/round, this is a misbehavior. | |||
3. lunatic validator: Tendermint consensus forces correct validators to prevote and precommit only for values *v* that satisfy *valid(v)*. If faulty validators prevote and precommit for *v* although *valid(v)=false* this is misbehavior. | |||
*Remark.* In isolation, Point 3 is an attack on validity (rather than agreement). However, the prevotes and precommits can then also be used to forge blocks. | |||
1. amnesia: Tendermint consensus has a locking mechanism. If a validator has some value v locked, then it can only prevote/precommit for v or nil. Sending prevote/precomit message for a different value v' (that is not nil) while holding lock on value v is misbehavior. | |||
2. spurious messages: In Tendermint consensus most of the message send instructions are guarded by threshold guards, e.g., one needs to receive *2f + 1* prevote messages to send precommit. Faulty validators may send precommit without having received the prevote messages. | |||
Independently of a fork happening, punishing this behavior might be important to prevent forks altogether. This should keep attackers from misbehaving: if at most 1/3 of the voting power is faulty, this misbehavior is detectable but will not lead to a safety violation. Thus, unless they have more than 1/3 (or in some cases more than 2/3) of the voting power attackers have the incentive to not misbehave. If attackers control too much voting power, we have to deal with forks, as discussed in this document. | |||
## Two types of forks | |||
* Fork-Full. Two correct validators decide on different blocks for the same height. Since also the next validator sets are decided upon, the correct validators may be partitioned to participate in two distinct branches of the forked chain. | |||
As in this case we have two different blocks (both having the same right/no right to exist), a central system invariant (one block per height decided by correct validators) is violated. As full nodes are contaminated in this case, the contamination can spread also to lite clients. However, even without breaking this system invariant, lite clients can be subject to a fork: | |||
* Fork-Lite. All correct validators decide on the same block for height *h*, but faulty processes (validators or not), forge a different block for that height, in order to fool users (who use the lite client). | |||
# Attack scenarios | |||
## On-chain attacks | |||
### Equivocation (one round) | |||
There are several scenarios in which forks might happen. The first is double signing within a round. | |||
* F1. Equivocation: faulty validators sign multiple vote messages (prevote and/or precommit) for different values *during the same round r* at a given height h. | |||
### Flip-flopping | |||
Tendermint consensus implements a locking mechanism: If a correct validator *p* receives proposal for value v and *2f + 1* prevotes for a value *id(v)* in round *r*, it locks *v* and remembers *r*. In this case, *p* also sends a precommit message for *id(v)*, which later may serve as proof that *p* locked *v*. | |||
In subsequent rounds, *p* only sends prevote messages for a value it had previously locked. However, it is possible to change the locked value if in a future round *r' > r*, if the process receives proposal and *2f + 1* prevotes for a different value *v'*. In this case, *p* could send a prevote/precommit for *id(v')*. This algorithmic feature can be exploited in two ways: | |||
* F2. Faulty Flip-flopping (Amnesia): faulty validators precommit some value *id(v)* in round *r* (value *v* is locked in round *r*) and then prevote for different value *id(v')* in higher round *r' > r* without previously correctly unlocking value *v*. In this case faulty processes "forget" that they have locked value *v* and prevote some other value in the following rounds. | |||
Some correct validators might have decided on *v* in *r*, and other correct validators decide on *v'* in *r'*. Here we can have branching on the main chain (Fork-Full). | |||
* F3. Correct Flip-flopping (Back to the past): There are some precommit messages signed by (correct) validators for value *id(v)* in round *r*. Still, *v* is not decided upon, and all processes move on to the next round. Then correct validators (correctly) lock and decide a different value *v'* in some round *r' > r*. And the correct validators continue; there is no branching on the main chain. | |||
However, faulty validators may use the correct precommit messages from round *r* together with a posteriori generated faulty precommit messages for round *r* to forge a block for a value that was not decided on the main chain (Fork-Lite). | |||
## Off-chain attacks | |||
F1-F3 may contaminate the state of full nodes (and even validators). Contaminated (but otherwise correct) full nodes may thus communicate faulty blocks to lite clients. | |||
Similarly, without actually interfering with the main chain, we can have the following: | |||
* F4. Phantom validators: faulty validators vote (sign prevote and precommit messages) in heights in which they are not part of the validator sets (at the main chain). | |||
* F5. Lunatic validator: faulty validator that sign vote messages to support (arbitrary) application state that is different from the application state that resulted from valid state transitions. | |||
## Types of victims | |||
We consider three types of potential attack victims: | |||
- FN: full node | |||
- LCS: lite client with sequential header verification | |||
- LCB: lite client with bisection based header verification | |||
F1 and F2 can be used by faulty validators to actually create multiple branches on the blockchain. That means that correctly operating full nodes decide on different blocks for the same height. Until a fork is detected locally by a full node (by receiving evidence from others or by some other local check that fails), the full node can spread corrupted blocks to lite clients. | |||
*Remark.* If full nodes take a branch different from the one taken by the validators, it may be that the liveness of the gossip protocol may be affected. We should eventually look at this more closely. However, as it does not influence safety it is not a primary concern. | |||
F3 is similar to F1, except that no two correct validators decide on different blocks. It may still be the case that full nodes become affected. | |||
In addition, without creating a fork on the main chain, lite clients can be contaminated by more than a third of validators that are faulty and sign a forged header | |||
F4 cannot fool correct full nodes as they know the current validator set. Similarly, LCS know who the validators are. Hence, F4 is an attack against LCB that do not necessarily know the complete prefix of headers (Fork-Lite), as they trust a header that is signed by at least one correct validator (trusting period method). | |||
The following table gives an overview of how the different attacks may affect different nodes. F1-F3 are *on-chain* attacks so they can corrupt the state of full nodes. Then if a lite client (LCS or LCB) contacts a full node to obtain headers (or blocks), the corrupted state may propagate to the lite client. | |||
F4 and F5 are *off-chain*, that is, these attacks cannot be used to corrupt the state of full nodes (which have sufficient knowledge on the state of the chain to not be fooled). | |||
| Attack | FN | LCS | LCB | | |||
|:------:|:------:|:------:|:------:| | |||
| F1 | direct | FN | FN | | |||
| F2 | direct | FN | FN | | |||
| F3 | direct | FN | FN | | |||
| F4 | | | direct | | |||
| F5 | | | direct | | |||
**Q:** Lite clients are more vulnerable than full nodes, because the former do only verify headers but do not execute transactions. What kind of certainty is gained by a full node that executes a transaction? | |||
As a full node verifies all transactions, it can only be | |||
contaminated by an attack if the blockchain itself violates its invariant (one block per height), that is, in case of a fork that leads to branching. | |||
## Detailed Attack Scenarios | |||
### Equivocation based attacks | |||
In case of equivocation based attacks, faulty validators sign multiple votes (prevote and/or precommit) in the same | |||
round of some height. This attack can be executed on both full nodes and lite clients. It requires more than 1/3 of voting power to be executed. | |||
#### Scenario 1: Equivocation on the main chain | |||
Validators: | |||
* CA - a set of correct validators with less than 1/3 of the voting power | |||
* CB - a set of correct validators with less than 1/3 of the voting power | |||
* CA and CB are disjoint | |||
* F - a set of faulty validators with more than 1/3 voting power | |||
Observe that this setting violates the Tendermint failure model. | |||
Execution: | |||
* A faulty proposer proposes block A to CA | |||
* A faulty proposer proposes block B to CB | |||
* Validators from the set CA and CB prevote for A and B, respectively. | |||
* Faulty validators from the set F prevote both for A and B. | |||
* The faulty prevote messages | |||
- for A arrive at CA long before the B messages | |||
- for B arrive at CB long before the A messages | |||
* Therefore correct validators from set CA and CB will observe | |||
more than 2/3 of prevotes for A and B and precommit for A and B, respectively. | |||
* Faulty validators from the set F precommit both values A and B. | |||
* Thus, we have more than 2/3 commits for both A and B. | |||
Consequences: | |||
* Creating evidence of misbehavior is simple in this case as we have multiple messages signed by the same faulty processes for different values in the same round. | |||
* We have to ensure that these different messages reach a correct process (full node, monitor?), which can submit evidence. | |||
* This is an attack on the full node level (Fork-Full). | |||
* It extends also to the lite clients, | |||
* For both we need a detection and recovery mechanism. | |||
#### Scenario 2: Equivocation to a lite client (LCS) | |||
Validators: | |||
* a set F of faulty validators with more than 2/3 of the voting power. | |||
Execution: | |||
* for the main chain F behaves nicely | |||
* F coordinates to sign a block B that is different from the one on the main chain. | |||
* the lite clients obtains B and trusts at as it is signed by more than 2/3 of the voting power. | |||
Consequences: | |||
Once equivocation is used to attack lite client it opens space | |||
for different kind of attacks as application state can be diverged in any direction. For example, it can modify validator set such that it contains only validators that do not have any stake bonded. Note that after a lite client is fooled by a fork, that means that an attacker can change application state and validator set arbitrarily. | |||
In order to detect such (equivocation-based attack), the lite client would need to cross check its state with some correct validator (or to obtain a hash of the state from the main chain using out of band channels). | |||
*Remark.* The lite client would be able to create evidence of misbehavior, but this would require to pull potentially a lot of data from correct full nodes. Maybe we need to figure out different architecture where a lite client that is attacked will push all its data for the current unbonding period to a correct node that will inspect this data and submit corresponding evidence. There are also architectures that assumes a special role (sometimes called fisherman) whose goal is to collect as much as possible useful data from the network, to do analysis and create evidence transactions. That functionality is outside the scope of this document. | |||
*Remark.* The difference between LCS and LCB might only be in the amount of voting power needed to convince lite client about arbitrary state. In case of LCB where security threshold is at minimum, an attacker can arbitrarily modify application state with more than 1/3 of voting power, while in case of LCS it requires more than 2/3 of the voting power. | |||
### Flip-flopping: Amnesia based attacks | |||
In case of amnesia, faulty validators lock some value *v* in some round *r*, and then vote for different value *v'* in higher rounds without correctly unlocking value *v*. This attack can be used both on full nodes and lite clients. | |||
#### Scenario 3: At most 2/3 of faults | |||
Validators: | |||
* a set F of faulty validators with more than 1/3 but at most 2/3 of the voting power | |||
* a set C of correct validators | |||
Execution: | |||
* Faulty validators commit (without exposing it on the main chain) a block A in round *r* by collecting more than 2/3 of the | |||
voting power (containing correct and faulty validators). | |||
* All validators (correct and faulty) reach a round *r' > r*. | |||
* Some correct validators in C do not lock any value before round *r'*. | |||
* The faulty validators in F deviate from Tendermint consensus by ignoring that they locked A in *r*, and propose a different block B in *r'*. | |||
* As the validators in C that have not locked any value find B acceptable, they accept the proposal for B and commit a block B. | |||
*Remark.* In this case, the more than 1/3 of faulty validators do not need to commit an equivocation (F1) as they only vote once per round in the execution. | |||
Detecting faulty validators in the case of such an attack can be done by the fork accountability mechanism described in: https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit?usp=sharing. | |||
If a lite client is attacked using this attack with more than 1/3 of voting power (and less than 2/3), the attacker cannot change the application state arbitrarily. Rather, the attacker is limited to a state a correct validator finds acceptable: In the execution above, correct validators still find the value acceptable, however, the block the lite client trusts deviates from the one on the main chain. | |||
#### Scenario 4: More than 2/3 of faults | |||
In case there is an attack with more than 2/3 of the voting power, an attacker can arbitrarily change application state. | |||
Validators: | |||
* a set F1 of faulty validators with more than 1/3 of the voting power | |||
* a set F2 of faulty validators with at most 1/3 of the voting power | |||
Execution | |||
* Similar to Scenario 3 (however, messages by correct validators are not needed) | |||
* The faulty validators in F1 lock value A in round *r* | |||
* They sign a different value in follow-up rounds | |||
* F2 does not lock A in round *r* | |||
Consequences: | |||
* The validators in F1 will be detectable by the the fork accountability mechanisms. | |||
* The validators in F2 cannot be detected using this mechanism. | |||
Only in case they signed something which conflicts with the application this can be used against them. Otherwise they do not do anything incorrect. | |||
* This case is not covered by the report https://docs.google.com/document/d/11ZhMsCj3y7zIZz4udO9l25xqb0kl7gmWqNpGVRzOeyY/edit?usp=sharing as it only assumes at most 2/3 of faulty validators. | |||
**Q:** do we need to define a special kind of attack for the case where a validator sign arbitrarily state? It seems that detecting such attack requires a different mechanism that would require as an evidence a sequence of blocks that led to that state. This might be very tricky to implement. | |||
### Back to the past | |||
In this kind of attack, faulty validators take advantage of the fact that they did not sign messages in some of the past rounds. Due to the asynchronous network in which Tendermint operates, we cannot easily differentiate between such an attack and delayed message. This kind of attack can be used at both full nodes and lite clients. | |||
#### Scenario 5: | |||
Validators: | |||
* C1 - a set of correct validators with 1/3 of the voting power | |||
* C2 - a set of correct validators with 1/3 of the voting power | |||
* C1 and C2 are disjoint | |||
* F - a set of faulty validators with 1/3 voting power | |||
* one additional faulty process *q* | |||
* F and *q* violate the Tendermint failure model. | |||
Execution: | |||
* in a round *r* of height *h* we have C1 precommitting a value A, | |||
* C2 precommits nil, | |||
* F does not send any message | |||
* *q* precommits nil. | |||
* In some round *r' > r*, F and *q* and C2 commit some other value B different from A. | |||
* F and *fp* "go back to the past" and sign precommit message for value A in round *r*. | |||
* Together with precomit messages of C1 this is sufficient for a commit for value A. | |||
Consequences: | |||
* Only a single faulty validator that previously precommited nil did equivocation, while the other 1/3 of faulty validators actually executed an attack that has exactly the same sequence of messages as part of amnesia attack. Detecting this kind of attack boil down to mechanisms for equivocation and amnesia. | |||
**Q:** should we keep this as a separate kind of attack? It seems that equivocation, amnesia and phantom validators are the only kind of attack we need to support and this gives us security also in other cases. This would not be surprising as equivocation and amnesia are attacks that followed from the protocol and phantom attack is not really an attack to Tendermint but more to the Proof of Stake module. | |||
### Phantom validators | |||
In case of phantom validators, processes that are not part of the current validator set but are still bonded (as attack happen during their unbonding period) can be part of the attack by signing vote messages. This attack can be executed against both full nodes and lite clients. | |||
#### Scenario 6: | |||
Validators: | |||
* F -- a set of faulty validators that are not part of the validator set on the main chain at height *h + k* | |||
Execution: | |||
* There is a fork, and there exist two different headers for height *h + k*, with different validator sets: | |||
- VS2 on the main chain | |||
- forged header VS2', signed by F (and others) | |||
* a lite client has a trust in a header for height *h* (and the corresponding validator set VS1). | |||
* As part of bisection header verification, it verifies the header at height *h + k* with new validator set VS2'. | |||
Consequences: | |||
* To detect this, a node needs to see both, the forged header and the canonical header from the chain. | |||
* If this is the case, detecting these kind of attacks is easy as it just requires verifying if processes are signing messages in heights in which they are not part of the validator set. | |||
**Remark.** We can have phantom-validator-based attacks as a follow up of equivocation or amnesia based attack where forked state contains validators that are not part of the validator set at the main chain. In this case, they keep signing messages contributed to a forked chain (the wrong branch) although they are not part of the validator set on the main chain. This attack can also be used to attack full node during a period of time it is eclipsed. | |||
### Lunatic validator | |||
Lunatic validator agrees to sign commit messages for arbitrary application state. It is used to attack lite clients. | |||
Note that detecting this behavior require application knowledge. Detecting this behavior can probably be done by | |||
referring to the block before the one in which height happen. | |||
**Q:** can we say that in this case a validator declines to check if a proposed value is valid before voting for it? |
@ -0,0 +1,3 @@ | |||
# Detection | |||
TODO |
@ -0,0 +1,579 @@ | |||
# Core Verification | |||
## Problem statement | |||
We assume that the light client knows a (base) header `inithead` it trusts (by social consensus or because | |||
the light client has decided to trust the header before). The goal is to check whether another header | |||
`newhead` can be trusted based on the data in `inithead`. | |||
The correctness of the protocol is based on the assumption that `inithead` was generated by an instance of | |||
Tendermint consensus. | |||
### Failure Model | |||
For the purpose of the following definitions we assume that there exists a function | |||
`validators` that returns the corresponding validator set for the given hash. | |||
The light client protocol is defined with respect to the following failure model: | |||
Given a known bound `TRUSTED_PERIOD`, and a block `b` with header `h` generated at time `Time` | |||
(i.e. `h.Time = Time`), a set of validators that hold more than 2/3 of the voting power | |||
in `validators(b.Header.NextValidatorsHash)` is correct until time `b.Header.Time + TRUSTED_PERIOD`. | |||
*Assumption*: "correct" is defined w.r.t. realtime (some Newtonian global notion of time, i.e., wall time), | |||
while `Header.Time` corresponds to the [BFT time](./../bft-time.md). In this note, we assume that clocks of correct processes | |||
are synchronized (for example using NTP), and therefore there is bounded clock drift (`CLOCK_DRIFT`) between local clocks and | |||
BFT time. More precisely, for every correct light client process and every `header.Time` (i.e. BFT Time, for a header correctly | |||
generated by the Tendermint consensus), the following inequality holds: `Header.Time < now + CLOCK_DRIFT`, | |||
where `now` corresponds to the system clock at the light client process. | |||
Furthermore, we assume that `TRUSTED_PERIOD` is (several) order of magnitude bigger than `CLOCK_DRIFT` (`TRUSTED_PERIOD >> CLOCK_DRIFT`), | |||
as `CLOCK_DRIFT` (using NTP) is in the order of milliseconds and `TRUSTED_PERIOD` is in the order of weeks. | |||
We expect a light client process defined in this document to be used in the context in which there is some | |||
larger period during which misbehaving validators can be detected and punished (we normally refer to it as `UNBONDING_PERIOD` | |||
due to the "bonding" mechanism in modern proof of stake systems). Furthermore, we assume that | |||
`TRUSTED_PERIOD < UNBONDING_PERIOD` and that they are normally of the same order of magnitude, for example | |||
`TRUSTED_PERIOD = UNBONDING_PERIOD / 2`. | |||
The specification in this document considers an implementation of the light client under the Failure Model defined above. | |||
Mechanisms like `fork accountability` and `evidence submission` are defined in the context of `UNBONDING_PERIOD` and | |||
they incentivize validators to follow the protocol specification defined in this document. If they don't, | |||
and we have 1/3 (or more) faulty validators, safety may be violated. Our approach then is | |||
to *detect* these cases (after the fact), and take suitable repair actions (automatic and social). | |||
This is discussed in document on [Fork accountability](./accountability.md). | |||
The term "trusted" above indicates that the correctness of the protocol depends on | |||
this assumption. It is in the responsibility of the user that runs the light client to make sure that the risk | |||
of trusting a corrupted/forged `inithead` is negligible. | |||
*Remark*: This failure model might change to a hybrid version that takes heights into account in the future. | |||
### High Level Solution | |||
Upon initialization, the light client is given a header `inithead` it trusts (by | |||
social consensus). When a light clients sees a new signed header `snh`, it has to decide whether to trust the new | |||
header. Trust can be obtained by (possibly) the combination of three methods. | |||
1. **Uninterrupted sequence of headers.** Given a trusted header `h` and an untrusted header `h1`, | |||
the light client trusts a header `h1` if it trusts all headers in between `h` and `h1`. | |||
2. **Trusted period.** Given a trusted header `h`, an untrusted header `h1 > h` and `TRUSTED_PERIOD` during which | |||
the failure model holds, we can check whether at least one validator, that has been continuously correct | |||
from `h.Time` until now, has signed `h1`. If this is the case, we can trust `h1`. | |||
3. **Bisection.** If a check according to 2. (trusted period) fails, the light client can try to | |||
obtain a header `hp` whose height lies between `h` and `h1` in order to check whether `h` can be used to | |||
get trust for `hp`, and `hp` can be used to get trust for `snh`. If this is the case we can trust `h1`; | |||
if not, we continue recursively until either we found set of headers that can build (transitively) trust relation | |||
between `h` and `h1`, or we failed as two consecutive headers don't verify against each other. | |||
## Definitions | |||
### Data structures | |||
In the following, only the details of the data structures needed for this specification are given. | |||
```go | |||
type Header struct { | |||
Height int64 | |||
Time Time // the chain time when the header (block) was generated | |||
LastBlockID BlockID // prev block info | |||
ValidatorsHash []byte // hash of the validators for the current block | |||
NextValidatorsHash []byte // hash of the validators for the next block | |||
} | |||
type SignedHeader struct { | |||
Header Header | |||
Commit Commit // commit for the given header | |||
} | |||
type ValidatorSet struct { | |||
Validators []Validator | |||
TotalVotingPower int64 | |||
} | |||
type Validator struct { | |||
Address Address // validator address (we assume validator's addresses are unique) | |||
VotingPower int64 // validator's voting power | |||
} | |||
type TrustedState { | |||
SignedHeader SignedHeader | |||
ValidatorSet ValidatorSet | |||
} | |||
``` | |||
### Functions | |||
For the purpose of this light client specification, we assume that the Tendermint Full Node | |||
exposes the following functions over Tendermint RPC: | |||
```go | |||
// returns signed header: Header with Commit, for the given height | |||
func Commit(height int64) (SignedHeader, error) | |||
// returns validator set for the given height | |||
func Validators(height int64) (ValidatorSet, error) | |||
``` | |||
Furthermore, we assume the following auxiliary functions: | |||
```go | |||
// returns true if the commit is for the header, ie. if it contains | |||
// the correct hash of the header; otherwise false | |||
func matchingCommit(header Header, commit Commit) bool | |||
// returns the set of validators from the given validator set that | |||
// committed the block (that correctly signed the block) | |||
// it assumes signature verification so it can be computationally expensive | |||
func signers(commit Commit, validatorSet ValidatorSet) []Validator | |||
// returns the voting power the validators in v1 have according to their voting power in set v2 | |||
// it does not assume signature verification | |||
func votingPowerIn(v1 []Validator, v2 ValidatorSet) int64 | |||
// returns hash of the given validator set | |||
func hash(v2 ValidatorSet) []byte | |||
``` | |||
### Functions | |||
In the functions below we will be using `trustThreshold` as a parameter. For simplicity | |||
we assume that `trustThreshold` is a float between `1/3` and `2/3` and we will not be checking it | |||
in the pseudo-code. | |||
**VerifySingle.** The function `VerifySingle` attempts to validate given untrusted header and the corresponding validator sets | |||
based on a given trusted state. It ensures that the trusted state is still within its trusted period, | |||
and that the untrusted header is within assumed `clockDrift` bound of the passed time `now`. | |||
Note that this function is not making external (RPC) calls to the full node; the whole logic is | |||
based on the local (given) state. This function is supposed to be used by the IBC handlers. | |||
```go | |||
func VerifySingle(untrustedSh SignedHeader, | |||
untrustedVs ValidatorSet, | |||
untrustedNextVs ValidatorSet, | |||
trustedState TrustedState, | |||
trustThreshold float, | |||
trustingPeriod Duration, | |||
clockDrift Duration, | |||
now Time) (TrustedState, error) { | |||
if untrustedSh.Header.Time > now + clockDrift { | |||
return (trustedState, ErrInvalidHeaderTime) | |||
} | |||
trustedHeader = trustedState.SignedHeader.Header | |||
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { | |||
return (state, ErrHeaderNotWithinTrustedPeriod) | |||
} | |||
// we assume that time it takes to execute verifySingle function | |||
// is several order of magnitudes smaller than trustingPeriod | |||
error = verifySingle( | |||
trustedState, | |||
untrustedSh, | |||
untrustedVs, | |||
untrustedNextVs, | |||
trustThreshold) | |||
if error != nil return (state, error) | |||
// the untrusted header is now trusted | |||
newTrustedState = TrustedState(untrustedSh, untrustedNextVs) | |||
return (newTrustedState, nil) | |||
} | |||
// return true if header is within its light client trusted period; otherwise returns false | |||
func isWithinTrustedPeriod(header Header, | |||
trustingPeriod Duration, | |||
now Time) bool { | |||
return header.Time + trustedPeriod > now | |||
} | |||
``` | |||
Note that in case `VerifySingle` returns without an error (untrusted header | |||
is successfully verified) then we have a guarantee that the transition of the trust | |||
from `trustedState` to `newTrustedState` happened during the trusted period of | |||
`trustedState.SignedHeader.Header`. | |||
TODO: Explain what happens in case `VerifySingle` returns with an error. | |||
**verifySingle.** The function `verifySingle` verifies a single untrusted header | |||
against a given trusted state. It includes all validations and signature verification. | |||
It is not publicly exposed since it does not check for header expiry (time constraints) | |||
and hence it's possible to use it incorrectly. | |||
```go | |||
func verifySingle(trustedState TrustedState, | |||
untrustedSh SignedHeader, | |||
untrustedVs ValidatorSet, | |||
untrustedNextVs ValidatorSet, | |||
trustThreshold float) error { | |||
untrustedHeader = untrustedSh.Header | |||
untrustedCommit = untrustedSh.Commit | |||
trustedHeader = trustedState.SignedHeader.Header | |||
trustedVs = trustedState.ValidatorSet | |||
if trustedHeader.Height >= untrustedHeader.Height return ErrNonIncreasingHeight | |||
if trustedHeader.Time >= untrustedHeader.Time return ErrNonIncreasingTime | |||
// validate the untrusted header against its commit, vals, and next_vals | |||
error = validateSignedHeaderAndVals(untrustedSh, untrustedVs, untrustedNextVs) | |||
if error != nil return error | |||
// check for adjacent headers | |||
if untrustedHeader.Height == trustedHeader.Height + 1 { | |||
if trustedHeader.NextValidatorsHash != untrustedHeader.ValidatorsHash { | |||
return ErrInvalidAdjacentHeaders | |||
} | |||
} else { | |||
error = verifyCommitTrusting(trustedVs, untrustedCommit, untrustedVs, trustThreshold) | |||
if error != nil return error | |||
} | |||
// verify the untrusted commit | |||
return verifyCommitFull(untrustedVs, untrustedCommit) | |||
} | |||
// returns nil if header and validator sets are consistent; otherwise returns error | |||
func validateSignedHeaderAndVals(signedHeader SignedHeader, vs ValidatorSet, nextVs ValidatorSet) error { | |||
header = signedHeader.Header | |||
if hash(vs) != header.ValidatorsHash return ErrInvalidValidatorSet | |||
if hash(nextVs) != header.NextValidatorsHash return ErrInvalidNextValidatorSet | |||
if !matchingCommit(header, signedHeader.Commit) return ErrInvalidCommitValue | |||
return nil | |||
} | |||
// returns nil if at least single correst signer signed the commit; otherwise returns error | |||
func verifyCommitTrusting(trustedVs ValidatorSet, | |||
commit Commit, | |||
untrustedVs ValidatorSet, | |||
trustLevel float) error { | |||
totalPower := trustedVs.TotalVotingPower | |||
signedPower := votingPowerIn(signers(commit, untrustedVs), trustedVs) | |||
// check that the signers account for more than max(1/3, trustLevel) of the voting power | |||
// this ensures that there is at least single correct validator in the set of signers | |||
if signedPower < max(1/3, trustLevel) * totalPower return ErrInsufficientVotingPower | |||
return nil | |||
} | |||
// returns nil if commit is signed by more than 2/3 of voting power of the given validator set | |||
// return error otherwise | |||
func verifyCommitFull(vs ValidatorSet, commit Commit) error { | |||
totalPower := vs.TotalVotingPower; | |||
signedPower := votingPowerIn(signers(commit, vs), vs) | |||
// check the signers account for +2/3 of the voting power | |||
if signedPower * 3 <= totalPower * 2 return ErrInvalidCommit | |||
return nil | |||
} | |||
``` | |||
**VerifyHeaderAtHeight.** The function `VerifyHeaderAtHeight` captures high level | |||
logic, i.e., application call to the light client module to download and verify header | |||
for some height. | |||
```go | |||
func VerifyHeaderAtHeight(untrustedHeight int64, | |||
trustedState TrustedState, | |||
trustThreshold float, | |||
trustingPeriod Duration, | |||
clockDrift Duration) (TrustedState, error)) { | |||
trustedHeader := trustedState.SignedHeader.Header | |||
now := System.Time() | |||
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { | |||
return (trustedState, ErrHeaderNotWithinTrustedPeriod) | |||
} | |||
newTrustedState, err := VerifyBisection(untrustedHeight, | |||
trustedState, | |||
trustThreshold, | |||
trustingPeriod, | |||
clockDrift, | |||
now) | |||
if err != nil return (trustedState, err) | |||
now = System.Time() | |||
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { | |||
return (trustedState, ErrHeaderNotWithinTrustedPeriod) | |||
} | |||
return (newTrustedState, err) | |||
} | |||
``` | |||
Note that in case `VerifyHeaderAtHeight` returns without an error (untrusted header | |||
is successfully verified) then we have a guarantee that the transition of the trust | |||
from `trustedState` to `newTrustedState` happened during the trusted period of | |||
`trustedState.SignedHeader.Header`. | |||
In case `VerifyHeaderAtHeight` returns with an error, then either (i) the full node we are talking to is faulty | |||
or (ii) the trusted header has expired (it is outside its trusted period). In case (i) the full node is faulty so | |||
light client should disconnect and reinitialise with new peer. In the case (ii) as the trusted header has expired, | |||
we need to reinitialise light client with a new trusted header (that is within its trusted period), | |||
but we don't necessarily need to disconnect from the full node we are talking to (as we haven't observed full node misbehavior in this case). | |||
**VerifyBisection.** The function `VerifyBisection` implements | |||
recursive logic for checking if it is possible building trust | |||
relationship between `trustedState` and untrusted header at the given height over | |||
finite set of (downloaded and verified) headers. | |||
```go | |||
func VerifyBisection(untrustedHeight int64, | |||
trustedState TrustedState, | |||
trustThreshold float, | |||
trustingPeriod Duration, | |||
clockDrift Duration, | |||
now Time) (TrustedState, error) { | |||
untrustedSh, error := Commit(untrustedHeight) | |||
if error != nil return (trustedState, ErrRequestFailed) | |||
untrustedHeader = untrustedSh.Header | |||
// note that we pass now during the recursive calls. This is fine as | |||
// all other untrusted headers we download during recursion will be | |||
// for a smaller heights, and therefore should happen before. | |||
if untrustedHeader.Time > now + clockDrift { | |||
return (trustedState, ErrInvalidHeaderTime) | |||
} | |||
untrustedVs, error := Validators(untrustedHeight) | |||
if error != nil return (trustedState, ErrRequestFailed) | |||
untrustedNextVs, error := Validators(untrustedHeight + 1) | |||
if error != nil return (trustedState, ErrRequestFailed) | |||
error = verifySingle( | |||
trustedState, | |||
untrustedSh, | |||
untrustedVs, | |||
untrustedNextVs, | |||
trustThreshold) | |||
if fatalError(error) return (trustedState, error) | |||
if error == nil { | |||
// the untrusted header is now trusted. | |||
newTrustedState = TrustedState(untrustedSh, untrustedNextVs) | |||
return (newTrustedState, nil) | |||
} | |||
// at this point in time we need to do bisection | |||
pivotHeight := ceil((trustedHeader.Height + untrustedHeight) / 2) | |||
error, newTrustedState = VerifyBisection(pivotHeight, | |||
trustedState, | |||
trustThreshold, | |||
trustingPeriod, | |||
clockDrift, | |||
now) | |||
if error != nil return (newTrustedState, error) | |||
return VerifyBisection(untrustedHeight, | |||
newTrustedState, | |||
trustThreshold, | |||
trustingPeriod, | |||
clockDrift, | |||
now) | |||
} | |||
func fatalError(err) bool { | |||
return err == ErrHeaderNotWithinTrustedPeriod OR | |||
err == ErrInvalidAdjacentHeaders OR | |||
err == ErrNonIncreasingHeight OR | |||
err == ErrNonIncreasingTime OR | |||
err == ErrInvalidValidatorSet OR | |||
err == ErrInvalidNextValidatorSet OR | |||
err == ErrInvalidCommitValue OR | |||
err == ErrInvalidCommit | |||
} | |||
``` | |||
### The case `untrustedHeader.Height < trustedHeader.Height` | |||
In the use case where someone tells the light client that application data that is relevant for it | |||
can be read in the block of height `k` and the light client trusts a more recent header, we can use the | |||
hashes to verify headers "down the chain." That is, we iterate down the heights and check the hashes in each step. | |||
*Remark.* For the case were the light client trusts two headers `i` and `j` with `i < k < j`, we should | |||
discuss/experiment whether the forward or the backward method is more effective. | |||
```go | |||
func VerifyHeaderBackwards(trustedHeader Header, | |||
untrustedHeader Header, | |||
trustingPeriod Duration, | |||
clockDrift Duration) error { | |||
if untrustedHeader.Height >= trustedHeader.Height return ErrErrNonDecreasingHeight | |||
if untrustedHeader.Time >= trustedHeader.Time return ErrNonDecreasingTime | |||
now := System.Time() | |||
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { | |||
return ErrHeaderNotWithinTrustedPeriod | |||
} | |||
old := trustedHeader | |||
for i := trustedHeader.Height - 1; i > untrustedHeader.Height; i-- { | |||
untrustedSh, error := Commit(i) | |||
if error != nil return ErrRequestFailed | |||
if (hash(untrustedSh.Header) != old.LastBlockID.Hash) { | |||
return ErrInvalidAdjacentHeaders | |||
} | |||
old := untrustedSh.Header | |||
} | |||
if hash(untrustedHeader) != old.LastBlockID.Hash { | |||
return ErrInvalidAdjacentHeaders | |||
} | |||
now := System.Time() | |||
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { | |||
return ErrHeaderNotWithinTrustedPeriod | |||
} | |||
return nil | |||
} | |||
``` | |||
*Assumption*: In the following, we assume that *untrusted_h.Header.height > trusted_h.Header.height*. We will quickly discuss the other case in the next section. | |||
We consider the following set-up: | |||
- the light client communicates with one full node | |||
- the light client locally stores all the headers that has passed basic verification and that are within light client trust period. In the pseudo code below we | |||
write *Store.Add(header)* for this. If a header failed to verify, then | |||
the full node we are talking to is faulty and we should disconnect from it and reinitialise with new peer. | |||
- If `CanTrust` returns *error*, then the light client has seen a forged header or the trusted header has expired (it is outside its trusted period). | |||
* In case of forged header, the full node is faulty so light client should disconnect and reinitialise with new peer. If the trusted header has expired, | |||
we need to reinitialise light client with new trusted header (that is within its trusted period), but we don't necessarily need to disconnect from the full node | |||
we are talking to (as we haven't observed full node misbehavior in this case). | |||
## Correctness of the Light Client Protocols | |||
### Definitions | |||
* `TRUSTED_PERIOD`: trusted period | |||
* for realtime `t`, the predicate `correct(v,t)` is true if the validator `v` | |||
follows the protocol until time `t` (we will see about recovery later). | |||
* Validator fields. We will write a validator as a tuple `(v,p)` such that | |||
+ `v` is the identifier (i.e., validator address; we assume identifiers are unique in each validator set) | |||
+ `p` is its voting power | |||
* For each header `h`, we write `trust(h) = true` if the light client trusts `h`. | |||
### Failure Model | |||
If a block `b` with a header `h` is generated at time `Time` (i.e. `h.Time = Time`), then a set of validators that | |||
hold more than `2/3` of the voting power in `validators(h.NextValidatorsHash)` is correct until time | |||
`h.Time + TRUSTED_PERIOD`. | |||
Formally, | |||
\[ | |||
\sum_{(v,p) \in validators(h.NextValidatorsHash) \wedge correct(v,h.Time + TRUSTED_PERIOD)} p > | |||
2/3 \sum_{(v,p) \in validators(h.NextValidatorsHash)} p | |||
\] | |||
The light client communicates with a full node and learns new headers. The goal is to locally decide whether to trust a header. Our implementation needs to ensure the following two properties: | |||
- *Light Client Completeness*: If a header `h` was correctly generated by an instance of Tendermint consensus (and its age is less than the trusted period), | |||
then the light client should eventually set `trust(h)` to `true`. | |||
- *Light Client Accuracy*: If a header `h` was *not generated* by an instance of Tendermint consensus, then the light client should never set `trust(h)` to true. | |||
*Remark*: If in the course of the computation, the light client obtains certainty that some headers were forged by adversaries | |||
(that is were not generated by an instance of Tendermint consensus), it may submit (a subset of) the headers it has seen as evidence of misbehavior. | |||
*Remark*: In Completeness we use "eventually", while in practice `trust(h)` should be set to true before `h.Time + TRUSTED_PERIOD`. If not, the header | |||
cannot be trusted because it is too old. | |||
*Remark*: If a header `h` is marked with `trust(h)`, but it is too old at some point in time we denote with `now` (`h.Time + TRUSTED_PERIOD < now`), | |||
then the light client should set `trust(h)` to `false` again at time `now`. | |||
*Assumption*: Initially, the light client has a header `inithead` that it trusts, that is, `inithead` was correctly generated by the Tendermint consensus. | |||
To reason about the correctness, we may prove the following invariant. | |||
*Verification Condition: light Client Invariant.* | |||
For each light client `l` and each header `h`: | |||
if `l` has set `trust(h) = true`, | |||
then validators that are correct until time `h.Time + TRUSTED_PERIOD` have more than two thirds of the voting power in `validators(h.NextValidatorsHash)`. | |||
Formally, | |||
\[ | |||
\sum_{(v,p) \in validators(h.NextValidatorsHash) \wedge correct(v,h.Time + TRUSTED_PERIOD)} p > | |||
2/3 \sum_{(v,p) \in validators(h.NextValidatorsHash)} p | |||
\] | |||
*Remark.* To prove the invariant, we will have to prove that the light client only trusts headers that were correctly generated by Tendermint consensus. | |||
Then the formula above follows from the failure model. | |||
## Details | |||
**Observation 1.** If `h.Time + TRUSTED_PERIOD > now`, we trust the validator set `validators(h.NextValidatorsHash)`. | |||
When we say we trust `validators(h.NextValidatorsHash)` we do `not` trust that each individual validator in `validators(h.NextValidatorsHash)` | |||
is correct, but we only trust the fact that less than `1/3` of them are faulty (more precisely, the faulty ones have less than `1/3` of the total voting power). | |||
*`VerifySingle` correctness arguments* | |||
Light Client Accuracy: | |||
- Assume by contradiction that `untrustedHeader` was not generated correctly and the light client sets trust to true because `verifySingle` returns without error. | |||
- `trustedState` is trusted and sufficiently new | |||
- by the Failure Model, less than `1/3` of the voting power held by faulty validators => at least one correct validator `v` has signed `untrustedHeader`. | |||
- as `v` is correct up to now, it followed the Tendermint consensus protocol at least up to signing `untrustedHeader` => `untrustedHeader` was correctly generated. | |||
We arrive at the required contradiction. | |||
Light Client Completeness: | |||
- The check is successful if sufficiently many validators of `trustedState` are still validators in the height `untrustedHeader.Height` and signed `untrustedHeader`. | |||
- If `untrustedHeader.Height = trustedHeader.Height + 1`, and both headers were generated correctly, the test passes. | |||
*Verification Condition:* We may need a Tendermint invariant stating that if `untrustedSignedHeader.Header.Height = trustedHeader.Height + 1` then | |||
`signers(untrustedSignedHeader.Commit) \subseteq validators(trustedHeader.NextValidatorsHash)`. | |||
*Remark*: The variable `trustThreshold` can be used if the user believes that relying on one correct validator is not sufficient. | |||
However, in case of (frequent) changes in the validator set, the higher the `trustThreshold` is chosen, the more unlikely it becomes that | |||
`verifySingle` returns with an error for non-adjacent headers. | |||
* `VerifyBisection` correctness arguments (sketch)* | |||
Light Client Accuracy: | |||
- Assume by contradiction that the header at `untrustedHeight` obtained from the full node was not generated correctly and | |||
the light client sets trust to true because `VerifyBisection` returns without an error. | |||
- `VerifyBisection` returns without error only if all calls to `verifySingle` in the recursion return without error (return `nil`). | |||
- Thus we have a sequence of headers that all satisfied the `verifySingle` | |||
- again a contradiction | |||
light Client Completeness: | |||
This is only ensured if upon `Commit(pivot)` the light client is always provided with a correctly generated header. | |||
*Stalling* | |||
With `VerifyBisection`, a faulty full node could stall a light client by creating a long sequence of headers that are queried one-by-one by the light client and look OK, | |||
before the light client eventually detects a problem. There are several ways to address this: | |||
* Each call to `Commit` could be issued to a different full node | |||
* Instead of querying header by header, the light client tells a full node which header it trusts, and the height of the header it needs. The full node responds with | |||
the header along with a proof consisting of intermediate headers that the light client can use to verify. Roughly, `VerifyBisection` would then be executed at the full node. | |||
* We may set a timeout how long `VerifyBisection` may take. | |||