diff --git a/spec/consensus/light-client.md b/spec/consensus/light-client.md index 434c0b721..8c07dfac8 100644 --- a/spec/consensus/light-client.md +++ b/spec/consensus/light-client.md @@ -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 } ```