- # ADR 046: Lite Client Implementation
-
- ## Changelog
- * 13-02-2020: Initial draft
- * 26-02-2020: Cross-checking the first header
- * 28-02-2020: Bisection algorithm details
-
- ## Context
-
- A `Client` struct represents a light client, connected to a single blockchain.
-
- The user has an option to verify headers using `VerifyHeader` or
- `VerifyHeaderAtHeight` or `Update` methods. The latter method downloads the
- latest header from primary and compares it with the currently trusted one.
-
- ```go
- type Client interface {
- // verify new headers
- VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error)
- VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error
- Update(now time.Time) (*types.SignedHeader, error)
-
- // get trusted headers & validators
- TrustedHeader(height int64) (*types.SignedHeader, error)
- TrustedValidatorSet(height int64) (valSet *types.ValidatorSet, heightUsed int64, err error)
- LastTrustedHeight() (int64, error)
- FirstTrustedHeight() (int64, error)
-
- // query configuration options
- ChainID() string
- Primary() provider.Provider
- Witnesses() []provider.Provider
-
- Cleanup() 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. Note the very first header (`TrustOptions.Hash`) is
- also cross-checked with witnesses for additional security.
-
- Due to bisection algorithm nature, some headers might be skipped. If the light
- client does not have a header for height `X` and `VerifyHeaderAtHeight(X)` or
- `VerifyHeader(H#X)` methods are called, these will perform either a) backwards
- verification from the latest header back to the header at height `X` or b)
- bisection verification from the first stored header to the header at height `X`.
-
- `TrustedHeader`, `TrustedValidatorSet` only communicate with the trusted store.
- If some header is not there, an error will be returned indicating that
- verification is required.
-
- ```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.
-
- ### Bisection algorithm details
-
- Non-recursive bisection algorithm was implemented despite the spec containing
- the recursive version. There are two major reasons:
-
- 1) Constant memory consumption => no risk of getting OOM (Out-Of-Memory) exceptions;
- 2) Faster finality (see Fig. 1).
-
- _Fig. 1: Differences between recursive and non-recursive bisections_
-
- ![Fig. 1](./img/adr-046-fig1.png)
-
- Specification of the non-recursive bisection can be found
- [here](https://github.com/tendermint/spec/blob/zm_non-recursive-verification/spec/consensus/light-client/non-recursive-verification.md).
-
- ## 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)
|