# Light client A light client is a process that connects to the Tendermint Full Node(s) and then tries to verify the Merkle proofs about the blockchain application. In this document we describe mechanisms that ensures that the Tendermint light client has the same level of security as Full Node processes (without being itself a Full Node). To be able to validate a Merkle proof, a light client needs to validate the blockchain header that contains the root app hash. Validating a blockchain header in Tendermint consists in verifying that the header is committed (signed) by >2/3 of the voting power of the corresponding validator set. As the validator set is a dynamic set (it is changing), one of the core functionality of the light client is updating the current validator set, that is then used to verify the blockchain header, and further the corresponding Merkle proofs. For the purpose of this light client specification, we assume that the Tendermint Full Node exposes the following functions over Tendermint RPC: ```golang Header(height int64) (SignedHeader, error) // returns signed header for the given height Validators(height int64) (ResultValidators, error) // returns validator set for the given height LastHeader(valSetNumber int64) (SignedHeader, error) // returns last header signed by the validator set with the given validator set number type SignedHeader struct { Header Header Commit Commit ValSetNumber int64 } type ResultValidators struct { BlockHeight int64 Validators []Validator // time the current validator set is initialised, i.e, time of the last validator change before header BlockHeight ValSetTime int64 } ``` We assume that Tendermint keeps track of the validator set changes and that each time a validator set is changed it is being assigned the next sequence number. We can call this number the validator set sequence number. Tendermint also remembers the Time from the header when the next validator set is initialised (starts to be in power), and we refer to this time as validator set init time. Furthermore, we assume that each validator set change is signed (committed) by the current validator set. More precisely, given a block `H` that contains transactions that are modifying the current validator set, the Merkle root hash of the next validator set (modified based on transactions from block H) will be in block `H+1` (and signed by the current validator set), and then starting from the block `H+2`, it will be signed by the next validator set. Note that the real Tendermint RPC API is slightly different (for example, response messages contain more data and function names are slightly different); we shortened (and modified) it for the purpose of this document to make the spec more clear and simple. Furthermore, note that in case of the third function, the returned header has `ValSetNumber` equals to `valSetNumber+1`. Locally, light client manages the following state: ```golang valSet []Validator // current validator set (last known and verified validator set) valSetNumber int64 // sequence number of the current validator set valSetHash []byte // hash of the current validator set valSetTime int64 // time when the current validator set is initialised ``` The light client is initialised with the trusted validator set, for example based on the known validator set hash, validator set sequence number and the validator set init time. The core of the light client logic is captured by the VerifyAndUpdate function that is used to 1) verify if the given header is valid, and 2) update the validator set (when the given header is valid and it is more recent than the seen headers). ```golang VerifyAndUpdate(signedHeader SignedHeader): assertThat signedHeader.valSetNumber >= valSetNumber if isValid(signedHeader) and signedHeader.Header.Time <= valSetTime + UNBONDING_PERIOD then setValidatorSet(signedHeader) return true else updateValidatorSet(signedHeader.ValSetNumber) return VerifyAndUpdate(signedHeader) isValid(signedHeader SignedHeader): valSetOfTheHeader = Validators(signedHeader.Header.Height) assertThat Hash(valSetOfTheHeader) == signedHeader.Header.ValSetHash assertThat signedHeader is passing basic validation if votingPower(signedHeader.Commit) > 2/3 * votingPower(valSetOfTheHeader) then return true else return false setValidatorSet(signedHeader SignedHeader): nextValSet = Validators(signedHeader.Header.Height) assertThat Hash(nextValSet) == signedHeader.Header.ValidatorsHash valSet = nextValSet.Validators valSetHash = signedHeader.Header.ValidatorsHash valSetNumber = signedHeader.ValSetNumber valSetTime = nextValSet.ValSetTime votingPower(commit Commit): votingPower = 0 for each precommit in commit.Precommits do: if precommit.ValidatorAddress is in valSet and signature of the precommit verifies then votingPower += valSet[precommit.ValidatorAddress].VotingPower return votingPower votingPower(validatorSet []Validator): for each validator in validatorSet do: votingPower += validator.VotingPower return votingPower updateValidatorSet(valSetNumberOfTheHeader): while valSetNumber != valSetNumberOfTheHeader do signedHeader = LastHeader(valSetNumber) if isValid(signedHeader) then setValidatorSet(signedHeader) else return error return ``` Note that in the logic above we assume that the light client will always go upward with respect to header verifications, i.e., that it will always be used to verify more recent headers. In case a light client needs to be used to verify older headers (go backward) the same mechanisms and similar logic can be used. In case a call to the FullNode or subsequent checks fail, a light client need to implement some recovery strategy, for example connecting to other FullNode.