|
|
@ -20,15 +20,10 @@ In the following, only the details of the data structures needed for this specif |
|
|
|
|
|
|
|
```go |
|
|
|
type Header struct { |
|
|
|
Height int64 |
|
|
|
Time Time // the chain time when the header (block) was generated |
|
|
|
|
|
|
|
// hashes from the app output from the prev block |
|
|
|
ValidatorsHash []byte // hash of the validators for the current block |
|
|
|
NextValidatorsHash []byte // hash of the validators for the next block |
|
|
|
|
|
|
|
// hashes of block data |
|
|
|
LastCommitHash []byte // hash of the commit from validators from the last block |
|
|
|
Height int64 |
|
|
|
Time Time // the chain time when the header (block) was generated |
|
|
|
ValidatorsHash []byte // hash of the validators for the current block |
|
|
|
NextValidatorsHash []byte // hash of the validators for the next block |
|
|
|
} |
|
|
|
|
|
|
|
type SignedHeader struct { |
|
|
@ -65,58 +60,67 @@ For the purpose of this lite client specification, we assume that the Tendermint |
|
|
|
|
|
|
|
Furthermore, we assume the following auxiliary functions: |
|
|
|
```go |
|
|
|
// returns the validator set for the given validator hash |
|
|
|
func validators(validatorsHash []byte) ValidatorSet |
|
|
|
|
|
|
|
// returns true if commit corresponds to the block data in the header; otherwise false |
|
|
|
// 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 |
|
|
|
// it does not assume signature verification |
|
|
|
// 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 |
|
|
|
|
|
|
|
// return the voting power the validators in v1 have according to their voting power in set v2 |
|
|
|
// it assumes signature verification so it can be computationally expensive |
|
|
|
// it does not assume signature verification |
|
|
|
func votingPowerIn(v1 []Validator, v2 ValidatorSet) int64 |
|
|
|
|
|
|
|
// add this state as trusted to the store |
|
|
|
func add(store Store, trustedState TrustedState) error |
|
|
|
|
|
|
|
// retrieve the trusted state at given height if it exists (error = nil) |
|
|
|
// return an error if there are no trusted state for the given height |
|
|
|
// if height = 0, return the latest trusted state |
|
|
|
func get(store Store, height int64) (TrustedState, error) |
|
|
|
// returns hash of the given validator set |
|
|
|
func hash(v2 ValidatorSet) []byte |
|
|
|
``` |
|
|
|
|
|
|
|
### Failure Model |
|
|
|
|
|
|
|
The lite client specification is defined with respect to the following failure model: If a block `b` is generated |
|
|
|
at time `Time` (and this time is stored in the block), then 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`. |
|
|
|
For the purpose of model definitions we assume that there exists a function |
|
|
|
`validators` that returns the corresponding validator set for the given hash. |
|
|
|
The lite client specification 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 process p and every header (correctly generated by the Tendermint consensus) |
|
|
|
time (BFT time) the following inequality holds: `Header.Time < now + CLOCK_DRIFT`. |
|
|
|
|
|
|
|
Furthermore, we assume that trust period is (several) order of magnitude bigger than clock drift (`TRUST_PERIOD >> CLOCK_DRIFT`), |
|
|
|
as clock drift (using NTP) is in the order of milliseconds and `TRUSTED_PERIOD` is in the order of weeks. |
|
|
|
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 lite 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 lite 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 lite 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 `PUNISHMENT_PERIOD`). |
|
|
|
Furthermore, we assume that `TRUSTED_PERIOD < PUNISHMENT_PERIOD` and that they are normally of the same order of magnitude, for example |
|
|
|
`TRUSTED_PERIOD = PUNISHMENT_PERIOD / 2`. Note that `PUNISHMENT_PERIOD` is often referred to as an |
|
|
|
unbonding period due to the "bonding" mechanism in modern proof of stake systems. |
|
|
|
|
|
|
|
The specification in this document considers an implementation of the lite client under the Failure Model defined above. |
|
|
|
Mechanisms like `fork accountability` and `evidence submission` are defined in the context of `PUNISHMENT_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](fork-accountability.md). |
|
|
|
|
|
|
|
*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 the Failure Model defined above. Issues |
|
|
|
like `counter-factual slashing`, `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 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](fork-accountability.md). |
|
|
|
|
|
|
|
### Functions |
|
|
|
|
|
|
|
**VerifyAndUpdateSingle.** The function `VerifyAndUpdateSingle` attempts to update |
|
|
|
the (trusted) store with the given untrusted header and the corresponding validator sets. |
|
|
|
It ensures that the last trusted header from the store hasn't expired yet (it is still within its trusted period), |
|
|
|
and that the untrusted header can be verified using the latest trusted state from the store. |
|
|
|
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 assume `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. |
|
|
|
|
|
|
@ -124,33 +128,35 @@ based on the local (given) state. This function is supposed to be used by the IB |
|
|
|
func VerifySingle(untrustedSh SignedHeader, |
|
|
|
untrustedVs ValidatorSet, |
|
|
|
untrustedNextVs ValidatorSet, |
|
|
|
trustThreshold TrustThreshold, |
|
|
|
trustedState TrustedState, |
|
|
|
trustThreshold float, |
|
|
|
trustingPeriod Duration, |
|
|
|
clockDrift Duration, |
|
|
|
now Time, |
|
|
|
trustedState TrustedState) error { |
|
|
|
now Time) (TrustedState, error) { |
|
|
|
|
|
|
|
assert untrustedSh.Header.Time < now + clockDrift |
|
|
|
if untrustedSh.Header.Time > now + clockDrift { |
|
|
|
return (trustedState, ErrInvalidHeaderTime) |
|
|
|
} |
|
|
|
|
|
|
|
trustedHeader = trustedState.SignedHeader.Header |
|
|
|
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { |
|
|
|
return (ErrHeaderNotWithinTrustedPeriod, nil) |
|
|
|
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) |
|
|
|
trustedState, |
|
|
|
untrustedSh, |
|
|
|
untrustedVs, |
|
|
|
untrustedNextVs, |
|
|
|
trustThreshold) |
|
|
|
|
|
|
|
if error != nil return (error, nil) |
|
|
|
if error != nil return (state, error) |
|
|
|
|
|
|
|
// the untrusted header is now trusted |
|
|
|
newTrustedState = TrustedState(untrustedSh, untrustedNextVs) |
|
|
|
return (nil, newTrustedState) |
|
|
|
return (newTrustedState, nil) |
|
|
|
} |
|
|
|
|
|
|
|
// return true if header is within its lite client trusted period; otherwise returns false |
|
|
@ -162,7 +168,7 @@ func isWithinTrustedPeriod(header Header, |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
Note that in case `VerifyAndUpdateSingle` returns without an error (untrusted header |
|
|
|
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`. |
|
|
@ -178,7 +184,7 @@ func verifySingle(trustedState TrustedState, |
|
|
|
untrustedSh SignedHeader, |
|
|
|
untrustedVs ValidatorSet, |
|
|
|
untrustedNextVs ValidatorSet, |
|
|
|
trustThreshold TrustThreshold) error { |
|
|
|
trustThreshold float) error { |
|
|
|
|
|
|
|
untrustedHeader = untrustedSh.Header |
|
|
|
untrustedCommit = untrustedSh.Commit |
|
|
@ -186,8 +192,8 @@ func verifySingle(trustedState TrustedState, |
|
|
|
trustedHeader = trustedState.SignedHeader.Header |
|
|
|
trustedVs = trustedState.ValidatorSet |
|
|
|
|
|
|
|
assert trustedHeader.Height < untrustedHeader.Height AND |
|
|
|
trustedHeader.Time < untrustedHeader.Time |
|
|
|
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) |
|
|
@ -199,7 +205,7 @@ func verifySingle(trustedState TrustedState, |
|
|
|
return ErrInvalidAdjacentHeaders |
|
|
|
} |
|
|
|
} else { |
|
|
|
error = verifyCommitTrusting(trustedVs, untrustedCommit, trustThreshold) |
|
|
|
error = verifyCommitTrusting(trustedVs, untrustedCommit, untrustedVs, trustThreshold) |
|
|
|
if error != nil return error |
|
|
|
} |
|
|
|
|
|
|
@ -210,17 +216,20 @@ func verifySingle(trustedState TrustedState, |
|
|
|
// 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(nextVs) != header.NextValidatorsHash OR |
|
|
|
hash(vs) != header.ValidatorsHash OR |
|
|
|
!matchingCommit(header, signedHeader.Commit) { return error } |
|
|
|
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(vs ValidatorSet, commit Commit, trustLevel TrustThreshold) error { |
|
|
|
func verifyCommitTrusting(trustedVs ValidatorSet, |
|
|
|
commit Commit, |
|
|
|
untrustedVs ValidatorSet, |
|
|
|
trustLevel float) error { |
|
|
|
|
|
|
|
totalPower := vs.TotalVotingPower |
|
|
|
signedPower := votingPowerIn(signers(commit, vs), vs) |
|
|
|
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 |
|
|
@ -232,10 +241,10 @@ func verifyCommitTrusting(vs ValidatorSet, commit Commit, trustLevel TrustThresh |
|
|
|
// return error otherwise |
|
|
|
func verifyCommitFull(vs ValidatorSet, commit Commit) error { |
|
|
|
totalPower := vs.TotalVotingPower; |
|
|
|
signed_power := votingPowerIn(signers(commit, vs), vs) |
|
|
|
signedPower := votingPowerIn(signers(commit, vs), vs) |
|
|
|
|
|
|
|
// check the signers account for +2/3 of the voting power |
|
|
|
if signed_power * 3 <= total_power * 2 return ErrInvalidCommit |
|
|
|
if signedPower * 3 <= totalPower * 2 return ErrInvalidCommit |
|
|
|
return nil |
|
|
|
} |
|
|
|
``` |
|
|
@ -246,73 +255,71 @@ for some height. |
|
|
|
|
|
|
|
```go |
|
|
|
func VerifyHeaderAtHeight(untrustedHeight int64, |
|
|
|
trustThreshold TrustThreshold, |
|
|
|
trustedState TrustedState, |
|
|
|
trustThreshold float, |
|
|
|
trustingPeriod Duration, |
|
|
|
clockDrift Duration, |
|
|
|
trustedState TrustedState) (error, TrustedState)) { |
|
|
|
clockDrift Duration) (TrustedState, error)) { |
|
|
|
|
|
|
|
trustedHeader := trustedState.SignedHeader.Header |
|
|
|
|
|
|
|
now := System.Time() |
|
|
|
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { |
|
|
|
return (ErrHeaderNotWithinTrustedPeriod, trustedState) |
|
|
|
return (trustedState, ErrHeaderNotWithinTrustedPeriod) |
|
|
|
} |
|
|
|
|
|
|
|
newTrustedState, err := VerifyAndUpdateBisection(trustedState, |
|
|
|
untrustedHeight, |
|
|
|
trustThreshold, |
|
|
|
trustingPeriod, |
|
|
|
clockDrift, |
|
|
|
now) |
|
|
|
newTrustedState, err := VerifyBisection(untrustedHeight, |
|
|
|
trustedState, |
|
|
|
trustThreshold, |
|
|
|
trustingPeriod, |
|
|
|
clockDrift, |
|
|
|
now) |
|
|
|
|
|
|
|
if err != nil return (err, trustedState) |
|
|
|
if err != nil return (trustedState, err) |
|
|
|
|
|
|
|
now = System.Time() |
|
|
|
if !isWithinTrustedPeriod(trustedHeader, trustingPeriod, now) { |
|
|
|
return (ErrHeaderNotWithinTrustedPeriod, trustedState) |
|
|
|
return (trustedState, ErrHeaderNotWithinTrustedPeriod) |
|
|
|
} |
|
|
|
|
|
|
|
return (nil, newTrustedState) |
|
|
|
return (newTrustedState, err) |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
Note that in case `VerifyAndUpdateSingle` returns without an error (untrusted header |
|
|
|
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`. |
|
|
|
|
|
|
|
**VerifyAndUpdateBisection.** The function `VerifyAndUpdateBisection` implements |
|
|
|
**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 |
|
|
|
relationship between `trustedState` and untrusted header at the given height over |
|
|
|
finite set of (downloaded and verified) headers. |
|
|
|
|
|
|
|
```go |
|
|
|
func VerifyAndUpdateBisection(trustedState TrustedState, |
|
|
|
untrustedHeight int64, |
|
|
|
trustThreshold TrustThreshold, |
|
|
|
trustingPeriod Duration, |
|
|
|
clockDrift Duration, |
|
|
|
now Time) (error, TrustedState) { |
|
|
|
|
|
|
|
trustedHeader = trustedState.SignedHeader.Header |
|
|
|
assert trustedHeader.Height < untrustedHeight |
|
|
|
func VerifyBisection(untrustedHeight int64, |
|
|
|
trustedState TrustedState, |
|
|
|
trustThreshold float, |
|
|
|
trustingPeriod Duration, |
|
|
|
clockDrift Duration, |
|
|
|
now Time) (TrustedState, error) { |
|
|
|
|
|
|
|
untrustedSh, error := Commit(untrustedHeight) |
|
|
|
if error != nil return (error, trustedState) |
|
|
|
if error != nil return (trustedState, ErrRequestFailed) |
|
|
|
|
|
|
|
untrustedHeader = untrustedSh.Header |
|
|
|
assert trustedHeader.Time < untrustedHeader.Time |
|
|
|
|
|
|
|
// 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. |
|
|
|
assert untrustedHeader.Time < now + clockDrift |
|
|
|
if untrustedHeader.Time > now + clockDrift { |
|
|
|
return (trustedState, ErrInvalidHeaderTime) |
|
|
|
} |
|
|
|
|
|
|
|
untrustedVs, error := Validators(untrustedHeight) |
|
|
|
if error != nil return (error, trustedState) |
|
|
|
if error != nil return (trustedState, ErrRequestFailed) |
|
|
|
|
|
|
|
untrustedNextVs, error := Validators(untrustedHeight + 1) |
|
|
|
if error != nil return (error, trustedState) |
|
|
|
if error != nil return (trustedState, ErrRequestFailed) |
|
|
|
|
|
|
|
error = verifySingle( |
|
|
|
trustedState, |
|
|
@ -321,38 +328,41 @@ func VerifyAndUpdateBisection(trustedState TrustedState, |
|
|
|
untrustedNextVs, |
|
|
|
trustThreshold) |
|
|
|
|
|
|
|
if fatalError(error) return (error, trustedState) |
|
|
|
if fatalError(error) return (trustedState, error) |
|
|
|
|
|
|
|
if error == nil { |
|
|
|
// the untrusted header is now trusted. |
|
|
|
newTrustedState = TrustedState(untrustedSh, untrustedNextVs) |
|
|
|
return (nil, newTrustedState) |
|
|
|
return (newTrustedState, nil) |
|
|
|
} |
|
|
|
|
|
|
|
// at this point in time we need to do bisection |
|
|
|
pivotHeight := ceil((trustedHeader.Height + untrustedHeight) / 2) |
|
|
|
|
|
|
|
error, newTrustedState = VerifyAndUpdateBisection(trustedState, |
|
|
|
pivotHeight, |
|
|
|
trustThreshold, |
|
|
|
trustingPeriod, |
|
|
|
clockDrift, |
|
|
|
now) |
|
|
|
if error != nil return (error, trustedState) |
|
|
|
|
|
|
|
error, newTrustedState = verifyAndUpdateBisection(newTrustedState, |
|
|
|
untrustedHeight, |
|
|
|
trustThreshold, |
|
|
|
trustingPeriod, |
|
|
|
clockDrift, |
|
|
|
now) |
|
|
|
if error != nil return (error, trustedState) |
|
|
|
return (nil, newTrustedState) |
|
|
|
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 |
|
|
|
} |
|
|
|
``` |
|
|
|