Files
tendermint/docs/architecture/adr-046-light-client-implementation.md
Anton Kaliaev c4f7256766 lite2: store current validator set (#4472)
Before we were storing trustedHeader (height=1) and trustedNextVals
(height=2).

After this change, we will be storing trustedHeader (height=1) and
trustedVals (height=1). This a) simplifies the code b) fixes #4399
inconsistent pairing issue c) gives a relayer access to the current
validator set #4470.

The only downside is more jumps during bisection. If validator set
changes between trustedHeader and the next header (by 2/3 or more), the
light client will be forced to download the next header and check that
2/3+ signed the transition. But we don't expect validator set change too
much and too often, so it's an acceptable compromise.

Closes #4470 and #4399
2020-02-26 10:20:51 +01:00

147 lines
4.7 KiB
Markdown

# ADR 046: Lite Client Implementation
## Changelog
* 13-02-2020: Initial draft
## Context
A `Client` struct represents a light client, connected to a single blockchain.
As soon as it's started (via `Start`), it tries to update to the latest header
(using bisection algorithm by default).
Cleaning routine is also started to remove headers outside of trusting period.
NOTE: since it's periodic, we still need to check header is not expired in
`TrustedHeader`, `TrustedValidatorSet` methods (and others which are using the
latest trusted header).
The user has an option to manually verify headers using `VerifyHeader` and
`VerifyHeaderAtHeight` methods. To avoid races, `UpdatePeriod(0)` needs to be
passed when initializing the light client (it turns off the auto update).
```go
type Client interface {
// start and stop updating & cleaning goroutines
Start() error
Stop()
Cleanup() error
// get trusted headers & validators
TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error)
TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error)
LastTrustedHeight() (int64, error)
FirstTrustedHeight() (int64, error)
// query configuration options
ChainID() string
Primary() provider.Provider
Witnesses() []provider.Provider
// verify new headers
VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error)
VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error
}
```
A new light client can either be created from scratch (via `NewClient`) or
using the trusted store (via `NewClientFromTrustedStore`). When there's some
data in the trusted store and `NewClient` is called, the light client will a)
check if stored header is more recent b) optionally ask the user whenever it
should rollback (no confirmation required by default).
```go
func NewClient(
chainID string,
trustOptions TrustOptions,
primary provider.Provider,
witnesses []provider.Provider,
trustedStore store.Store,
options ...Option) (*Client, error) {
```
`witnesses` as argument (as opposite to `Option`) is an intentional choice,
made to increase security by default. At least one witness is required,
although, right now, the light client does not check that primary != witness.
When cross-checking a new header with witnesses, minimum number of witnesses
required to respond: 1.
Due to bisection algorithm nature, some headers might be skipped. If the light
client does not have a header for height `X` and `TrustedHeader(X)` or
`TrustedValidatorSet(X)` methods are called, it will download the header from
primary provider and perform a backwards verification.
```go
type Provider interface {
ChainID() string
SignedHeader(height int64) (*types.SignedHeader, error)
ValidatorSet(height int64) (*types.ValidatorSet, error)
}
```
Provider is a full node usually, but can be another light client. The above
interface is thin and can accommodate many implementations.
If provider (primary or witness) becomes unavailable for a prolonged period of
time, it will be removed to ensure smooth operation.
Both `Client` and providers expose chain ID to track if there are on the same
chain. Note, when chain upgrades or intentionally forks, chain ID changes.
The light client stores headers & validators in the trusted store:
```go
type Store interface {
SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error
DeleteSignedHeaderAndValidatorSet(height int64) error
SignedHeader(height int64) (*types.SignedHeader, error)
ValidatorSet(height int64) (*types.ValidatorSet, error)
LastSignedHeaderHeight() (int64, error)
FirstSignedHeaderHeight() (int64, error)
SignedHeaderAfter(height int64) (*types.SignedHeader, error)
}
```
At the moment, the only implementation is the `db` store (wrapper around the KV
database, used in Tendermint). In the future, remote adapters are possible
(e.g. `Postgresql`).
```go
func Verify(
chainID string,
h1 *types.SignedHeader,
h1NextVals *types.ValidatorSet,
h2 *types.SignedHeader,
h2Vals *types.ValidatorSet,
trustingPeriod time.Duration,
now time.Time,
trustLevel tmmath.Fraction) error {
```
`Verify` pure function is exposed for a header verification. It handles both
cases of adjacent and non-adjacent headers. In the former case, it compares the
hashes directly (2/3+ signed transition). Otherwise, it verifies 1/3+
(`trustLevel`) of trusted validators are still present in new validators.
## Status
Accepted.
## Consequences
### Positive
* single `Client` struct, which is easy to use
* flexible interfaces for header providers and trusted storage
### Negative
* `Verify` needs to be aligned with the current spec
### Neutral
* `Verify` function might be misused (called with non-adjacent headers in
incorrectly implemented sequential verification)