mirror of
https://github.com/fluencelabs/tendermint
synced 2025-07-30 19:51:58 +00:00
Compare commits
2 Commits
v0.32.3
...
zm_light_c
Author | SHA1 | Date | |
---|---|---|---|
|
e32fa44d6f | ||
|
ef8e18a1f3 |
@@ -1,113 +1,145 @@
|
||||
# Light Client
|
||||
# Lite 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
|
||||
A lite client is a process that connects to the Tendermint Full Node(s) and then tries to verify application data using the Merkle proofs. In this document we describe mechanisms that ensures that the Tendermint lite 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.
|
||||
## Lite client requirements from Tendermint and Proof of Stake modules
|
||||
|
||||
For the purpose of this light client specification, we assume that the Tendermint Full Node exposes the following functions over
|
||||
Tendermint RPC:
|
||||
Before explaining lite client mechanisms and operations we need to define some requirements expected from the Tendermint blockchain. Tendermint provides a deterministic, Byzantine fault-tolerant, source of time (called
|
||||
[BFT Time](/Users/zarkomilosevic/go-workspace/src/github.com/tendermint/tendermint/docs/spec/consensus/bft-time.md)).
|
||||
BFT time is monotonically increasing and in case of at most 1/3 of voting power equivalent of faulty validators guaranteed to be close to the wall time of correct validators. For correct functioning of lite client we need a guarantee that BFT time does not drift more than some known parameter BFT_TIME_DRIFT_BOUND (that should normally be measured in hours, maybe even days) from client wall time. Note that this requirement currently only holds in case
|
||||
at most 1/3 of voting power equivalent of validators report wrong time, but we might need to strengthen this requirement further to also be able to tolerate time-related misbehavior of more than 1/3 voting power equivalent of validators (https://github.com/tendermint/tendermint/issues/2653, https://github.com/tendermint/tendermint/issues/2840).
|
||||
|
||||
Furthermore, lite client security is tightly coupled with the notion of UNBONDING_PERIOD that is at the core of the security of proof of stake blockchain systems (for example Cosmos Hub). UNBONDING_PERIOD is period of time that needs to pass from the withdraw event until stake is liquid. During this period unbonded validator cannot participate in the consensus protocol (and is therefore not rewarded) but can be slashed for misbehavior (done either before withdraw event or during UNBONDING_PERIOD). This is used to protect against a validator attacking the network and then immediately withdrawing his stake. Cosmos Hub is currently enforcing a 21-day UNBONDING_PERIOD. Note that UNBONDING_PERIOD is measured with respect to BFT time and that this has significant effect on the security of lite client operation as validators are not slashable outside their UNBONDING_PERIOD. There is a hidden implicit assumptions regarding the UNBONDING_PERIOD: we assume that Tendermint will always generate blocks within duration of UNBONDING_PERIOD. If chain halts for the duration of UNBONDING_PERIOD security of lite clients are jeopardized. Probably more secure solution would be defining UNBONDING_PERIOD as a hybrid of wall time and logical time (number of block heights). In that case UNBONDING_PERIOD is over when the both conditions are true. In that case no assumption is being made on the chain progress (which is in theory hard to make as Tendermint operate in partially synchronous system model), and system is secure (including lite clients) even in case of long halts.
|
||||
|
||||
## Lite client initialization
|
||||
|
||||
Lite client is initialized using a trusted socially verified validator set hash. This is part of the security model for
|
||||
proof of stake blockchains (see https://github.com/tendermint/tendermint/issues/1771). We assume that initialization
|
||||
procedure receive as input the following parameters:
|
||||
|
||||
- height h: height of the blockchain for which trusted validator set hash is provided and
|
||||
- trusted validator set hash.
|
||||
|
||||
Given this information, light client initialization logic will call `Validators` method on the node it is connected to
|
||||
obtain `ResultValidators` that contains validators that has committed the block h. Then we check if MerkleRoot
|
||||
of the validator set is equal to the trusted validator set hash. If verification failed, initialization exits with error, otherwise it proceeds.
|
||||
|
||||
Next step is determining if the block at hight h is correctly signed by the obtained validator set. This is achieved by
|
||||
calling `Commit` method for a given height that returns `SignedHeader` that is a header together with the set of
|
||||
`Precommit` messages that commit the corresponding block. Light client logic then needs to verify if the set of signatures defined the valid commit for the given block. If this verification fails, initialization exits with error, otherwise it proceeds.
|
||||
|
||||
Final step (this should actually be part of the second step) of the initialization procedure is ensuring that we operate within UNBONDING_PERIOD of the trusted validator set. This can be expressed with the following formula:
|
||||
|
||||
`header.Time + UNBONDING_PERIOD <= Now() - BFT_TIME_DRIFT_BOUND`.
|
||||
|
||||
Note that outside this time window lite client cannot trust validator set as validators could potentially unbonded its stake so security of the lite client does not hold as they are not slashable for its actions. Therefore, they can eclipse client and cheat about the system state without risk of being punished for such misbehavior.
|
||||
|
||||
Note that this formula shows a fundamental dependence of lite client security on the wall time. If UNBONDING_PERIOD
|
||||
would be defined only in terms of logical time (block heights), lite client will not have a way to know if trusted validator set is still withing its UNBONDING_PERIOD as it does not have a way of reliably determining top of the chain.
|
||||
Therefore, it seems that having BFT time in sync with standard notions of time (for example NTP) is necessarily for correct operations of the system.
|
||||
|
||||
Lite client security depends also on the guarantee that faulty validator behavior will be punished. Therefore if a client detect faulty behavior we need to guarantee that proof of misbehavior evidence transaction will be committed within UNBONDING_PERIOD of faulty validators so it can be slashed. This can be achieved by having client considering
|
||||
time period smaller than UNBONDING_PERIOD. Let's denote this time period LITE_CLIENT_CERTIFICATE_PERIOD, and we can assume that it's set to a fraction of UNBONDING_PERIOD. For example, it can be set to be half of UNBONDING_PERIOD.
|
||||
This normally leaves a lot of time to client to ensure that evidence transaction is committed by the network during
|
||||
the UNBONDING_PERIOD. As these periods are normally measured in days, a client can in the worst case rely on
|
||||
social channels to ensure that evidence transaction is proposed by some validator. Note that we might also assume that as part of validator service some validators might have "hot line" that can be used to submit evidence of misbehavior.
|
||||
|
||||
Given a trusted validator set whose trust is established by the header H, we say that this validator set is trusted the latest until `H.Time + LITE_CLIENT_CERTIFICATE_PERIOD`, i.e., the trusted period is defined by the following formula:
|
||||
`header.Time + LITE_CLIENT_CERTIFICATE_PERIOD <= Now() - BFT_TIME_DRIFT_BOUND`. If this formula does not hold, we say
|
||||
that trust in the given validator set is revoked and client need to establish a new root of trust.
|
||||
|
||||
For the purpose of this lite 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
|
||||
}
|
||||
Commit(height int64) (SignedHeader, error) // returns signed header for the given height
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type SignedHeader struct {
|
||||
Header Header
|
||||
Commit Commit
|
||||
}
|
||||
```
|
||||
Locally, lite client manages the following state:
|
||||
|
||||
```golang
|
||||
type LiteClientState struct {
|
||||
ValSet []Validator // validator set at the given height
|
||||
ValSetHash []byte // hash of the current validator set
|
||||
NextValSet []Validator // next validator set
|
||||
NextValSetHash []byte // hash of the next validator set
|
||||
Height int64 // current height
|
||||
ValSetTime int64 // time when the current and next validator set is initialized
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
Initialization procedure is captured by the following pseudo code:
|
||||
|
||||
```golang
|
||||
Init(height int64, valSetHash Hash): (LiteClientState, error) {
|
||||
state = LiteClientState {}
|
||||
valSet = Validators(height).Validators
|
||||
if Hash(valSet) != valSetHash then return (nil, error)
|
||||
|
||||
signedHeader = Commit(height)
|
||||
if signedHeader.Header.Time + LITE_CLIENT_CERTIFICATE_PERIOD > Now() - BFT_TIME_DRIFT_BOUND then return (nil, error)
|
||||
if votingPower(signedHeader.Commit) <= 2/3 totalVotingPower(valSet) then return (nil, error)
|
||||
|
||||
nextValSet = Validators(height+1).Validators
|
||||
if Hash(nextValSet) != signedHeader.Header.NextValSetHash then return (nil, error)
|
||||
|
||||
state.ValSet = nextValSet
|
||||
state.ValSetHash = signedHeader.Header.NextValSetHash
|
||||
state.Height = height
|
||||
state.ValSetTime = signedHeader.Header.Time
|
||||
|
||||
return state, nil
|
||||
}
|
||||
```
|
||||
|
||||
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 lite client is updating the current validator set, that is then used to verify the
|
||||
blockchain header, and further the corresponding Merkle proofs.
|
||||
|
||||
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) are 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:
|
||||
The core of the light client logic is captured by the VerifyAndUpdate function that is used to update
|
||||
trusted validator set to the one from the given height.
|
||||
|
||||
```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
|
||||
```
|
||||
VerifyAndUpdate(state LiteClientState, height int64): error {
|
||||
if signedHeader.Header.Height <= height then return error
|
||||
|
||||
// check if certification period of the current validator set is still secure
|
||||
if state.ValSetTime + LITE_CLIENT_CERTIFICATE_PERIOD > Now() - BFT_TIME_DRIFT_BOUND then return error
|
||||
|
||||
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).
|
||||
for i = state.Height+1; i < height; i++ {
|
||||
signedHeader = Commit(i)
|
||||
if votingPower(signedHeader.Commit) <= 2/3 totalVotingPower(state.ValSet) then return error
|
||||
if signedHeader.Header.ValidatorsHash != state.ValSetHash then return error
|
||||
|
||||
```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)
|
||||
nextValSetHash = signedHeader.Header.NextValidatorsHash
|
||||
|
||||
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
|
||||
nextValSet = Validators(i+1).Validators
|
||||
if Hash(nextValSet) != nextValSetHash then return (0, error)
|
||||
|
||||
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
|
||||
// at this point we can install new valset
|
||||
state.ValSet = nextValSet
|
||||
state.ValSetHash = nextValSetHash
|
||||
state.Height = i
|
||||
state.ValSetTime = signedHeader.Header.Time
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Note that in the logic above we assume that the lite client will always go upward with respect to header verifications,
|
||||
i.e., that it will always be used to verify more recent headers. Going backward is problematic as having trust in
|
||||
some validator set at height h does not give us trust in validator set before height h. Therefore, if lite client query
|
||||
is about smaller height, trusted validator set for smaller heights is needed.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
Reference in New Issue
Block a user