mirror of
https://github.com/fluencelabs/tendermint
synced 2025-04-25 14:52:17 +00:00
115 lines
5.8 KiB
Markdown
115 lines
5.8 KiB
Markdown
|
# 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.
|