- package lite
-
- import (
- "bytes"
- "fmt"
- "time"
-
- "github.com/pkg/errors"
-
- "github.com/tendermint/tendermint/libs/log"
- tmmath "github.com/tendermint/tendermint/libs/math"
- tmrand "github.com/tendermint/tendermint/libs/rand"
- "github.com/tendermint/tendermint/lite2/provider"
- "github.com/tendermint/tendermint/lite2/store"
- "github.com/tendermint/tendermint/types"
- )
-
- // TrustOptions are the trust parameters needed when a new light client
- // connects to the network or when an existing light client that has been
- // offline for longer than the trusting period connects to the network.
- //
- // The expectation is the user will get this information from a trusted source
- // like a validator, a friend, or a secure website. A more user friendly
- // solution with trust tradeoffs is that we establish an https based protocol
- // with a default end point that populates this information. Also an on-chain
- // registry of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the
- // future.
- type TrustOptions struct {
- // tp: trusting period.
- //
- // Should be significantly less than the unbonding period (e.g. unbonding
- // period = 3 weeks, trusting period = 2 weeks).
- //
- // More specifically, trusting period + time needed to check headers + time
- // needed to report and punish misbehavior should be less than the unbonding
- // period.
- Period time.Duration
-
- // Header's Height and Hash must both be provided to force the trusting of a
- // particular header.
- Height int64
- Hash []byte
- }
-
- type mode byte
-
- const (
- sequential mode = iota + 1
- skipping
- )
-
- // Option sets a parameter for the light client.
- type Option func(*Client)
-
- // SequentialVerification option configures the light client to sequentially
- // check the headers. Note this is much slower than SkippingVerification,
- // albeit more secure.
- func SequentialVerification() Option {
- return func(c *Client) {
- c.verificationMode = sequential
- }
- }
-
- // SkippingVerification option configures the light client to skip headers as
- // long as {trustLevel} of the old validator set signed the new header. The
- // bisection algorithm from the specification is used for finding the minimal
- // "trust path".
- //
- // trustLevel - fraction of the old validator set (in terms of voting power),
- // which must sign the new header in order for us to trust it. NOTE this only
- // applies to non-adjusted headers. For adjusted headers, sequential
- // verification is used.
- func SkippingVerification(trustLevel tmmath.Fraction) Option {
- if err := ValidateTrustLevel(trustLevel); err != nil {
- panic(err)
- }
- return func(c *Client) {
- c.verificationMode = skipping
- c.trustLevel = trustLevel
- }
- }
-
- // AlternativeSources option can be used to supply alternative providers, which
- // will be used for cross-checking the primary provider.
- func AlternativeSources(providers []provider.Provider) Option {
- return func(c *Client) {
- c.alternatives = providers
- }
- }
-
- // Client represents a light client, connected to a single chain, which gets
- // headers from a primary provider, verifies them either sequentially or by
- // skipping some and stores them in a trusted store (usually, a local FS).
- //
- // Default verification: SkippingVerification(DefaultTrustLevel)
- type Client struct {
- chainID string
- trustingPeriod time.Duration // see TrustOptions.Period
- verificationMode mode
- trustLevel tmmath.Fraction
-
- // Primary provider of new headers.
- primary provider.Provider
-
- // Alternative providers for checking the primary for misbehavior by
- // comparing data.
- alternatives []provider.Provider
-
- // Where trusted headers are stored.
- trustedStore store.Store
- // Highest trusted header from the store (height=H).
- trustedHeader *types.SignedHeader
- // Highest next validator set from the store (height=H+1).
- trustedNextVals *types.ValidatorSet
-
- logger log.Logger
- }
-
- // NewClient returns a new light client. It returns an error if it fails to
- // obtain the header & vals from the primary or they are invalid (e.g. trust
- // hash does not match with the one from the header).
- //
- // See all Option(s) for the additional configuration.
- func NewClient(
- chainID string,
- trustOptions TrustOptions,
- primary provider.Provider,
- trustedStore store.Store,
- options ...Option) (*Client, error) {
-
- c := &Client{
- chainID: chainID,
- trustingPeriod: trustOptions.Period,
- verificationMode: skipping,
- trustLevel: DefaultTrustLevel,
- primary: primary,
- trustedStore: trustedStore,
- logger: log.NewNopLogger(),
- }
-
- for _, o := range options {
- o(c)
- }
-
- if err := c.initializeWithTrustOptions(trustOptions); err != nil {
- return nil, err
- }
-
- return c, nil
- }
-
- func (c *Client) initializeWithTrustOptions(options TrustOptions) error {
- // 1) Fetch and verify the header.
- h, err := c.primary.SignedHeader(options.Height)
- if err != nil {
- return err
- }
-
- // NOTE: Verify func will check if it's expired or not.
- if err := h.ValidateBasic(c.chainID); err != nil {
- return errors.Wrap(err, "ValidateBasic failed")
- }
-
- if !bytes.Equal(h.Hash(), options.Hash) {
- return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash())
- }
-
- // 2) Fetch and verify the next vals.
- vals, err := c.primary.ValidatorSet(options.Height + 1)
- if err != nil {
- return err
- }
-
- // 3) Persist both of them and continue.
- return c.updateTrustedHeaderAndVals(h, vals)
- }
-
- // SetLogger sets a logger.
- func (c *Client) SetLogger(l log.Logger) {
- c.logger = l
- }
-
- // TrustedHeader returns a trusted header at the given height (0 - the latest)
- // or nil if no such header exist.
- // TODO: mention how many headers will be kept by the light client.
- // .
- // height must be >= 0.
- //
- // It returns an error if:
- // - the header expired (ErrOldHeaderExpired). In that case, update your
- // client to more recent height;
- // - there are some issues with the trusted store, although that should not
- // happen normally.
- func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) {
- if height < 0 {
- return nil, errors.New("negative height")
- }
-
- if height == 0 {
- var err error
- height, err = c.LastTrustedHeight()
- if err != nil {
- return nil, err
- }
- }
-
- h, err := c.trustedStore.SignedHeader(height)
- if err != nil {
- return nil, err
- }
-
- // Ensure header can still be trusted.
- expirationTime := h.Time.Add(c.trustingPeriod)
- if !expirationTime.After(now) {
- return nil, ErrOldHeaderExpired{expirationTime, now}
- }
-
- return h, nil
- }
-
- // LastTrustedHeight returns a last trusted height.
- func (c *Client) LastTrustedHeight() (int64, error) {
- return c.trustedStore.LastSignedHeaderHeight()
- }
-
- // ChainID returns the chain ID.
- func (c *Client) ChainID() string {
- return c.chainID
- }
-
- // VerifyHeaderAtHeight fetches the header and validators at the given height
- // and calls VerifyHeader.
- //
- // If the trusted header is more recent than one here, an error is returned.
- func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) error {
- if c.trustedHeader.Height >= height {
- return errors.Errorf("height #%d is already trusted (last: #%d)", height, c.trustedHeader.Height)
- }
-
- // Request the header and the vals.
- newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height)
- if err != nil {
- return err
- }
-
- return c.VerifyHeader(newHeader, newVals, now)
- }
-
- // VerifyHeader verifies new header against the trusted state.
- //
- // SequentialVerification: verifies that 2/3 of the trusted validator set has
- // signed the new header. If the headers are not adjacent, **all** intermediate
- // headers will be requested.
- //
- // SkippingVerification(trustLevel): verifies that {trustLevel} of the trusted
- // validator set has signed the new header. If it's not the case and the
- // headers are not adjacent, bisection is performed and necessary (not all)
- // intermediate headers will be requested. See the specification for the
- // algorithm.
- //
- // If the trusted header is more recent than one here, an error is returned.
- func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
- if c.trustedHeader.Height >= newHeader.Height {
- return errors.Errorf("height #%d is already trusted (last: #%d)", newHeader.Height, c.trustedHeader.Height)
- }
-
- if len(c.alternatives) > 0 {
- if err := c.compareNewHeaderWithRandomAlternative(newHeader); err != nil {
- return err
- }
- }
-
- var err error
- switch c.verificationMode {
- case sequential:
- err = c.sequence(newHeader, newVals, now)
- case skipping:
- err = c.bisection(c.trustedHeader, c.trustedNextVals, newHeader, newVals, now)
- default:
- panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode))
- }
- if err != nil {
- return err
- }
-
- // Update trusted header and vals.
- nextVals, err := c.primary.ValidatorSet(newHeader.Height + 1)
- if err != nil {
- return err
- }
- return c.updateTrustedHeaderAndVals(newHeader, nextVals)
- }
-
- func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
- // 1) Verify any intermediate headers.
- var (
- interimHeader *types.SignedHeader
- nextVals *types.ValidatorSet
- err error
- )
- for height := c.trustedHeader.Height + 1; height < newHeader.Height; height++ {
- interimHeader, err = c.primary.SignedHeader(height)
- if err != nil {
- return errors.Wrapf(err, "failed to obtain the header #%d", height)
- }
-
- err = Verify(c.chainID, c.trustedHeader, c.trustedNextVals, interimHeader, c.trustedNextVals,
- c.trustingPeriod, now, c.trustLevel)
- if err != nil {
- return errors.Wrapf(err, "failed to verify the header #%d", height)
- }
-
- // Update trusted header and vals.
- if height == newHeader.Height-1 {
- nextVals = newVals
- } else {
- nextVals, err = c.primary.ValidatorSet(height + 1)
- if err != nil {
- return errors.Wrapf(err, "failed to obtain the vals #%d", height+1)
- }
- }
- err = c.updateTrustedHeaderAndVals(interimHeader, nextVals)
- if err != nil {
- return errors.Wrapf(err, "failed to update trusted state #%d", height)
- }
- }
-
- // 2) Verify the new header.
- return Verify(c.chainID, c.trustedHeader, c.trustedNextVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel)
- }
-
- func (c *Client) bisection(
- lastHeader *types.SignedHeader,
- lastVals *types.ValidatorSet,
- newHeader *types.SignedHeader,
- newVals *types.ValidatorSet,
- now time.Time) error {
-
- err := Verify(c.chainID, lastHeader, lastVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel)
- switch err.(type) {
- case nil:
- return nil
- case types.ErrTooMuchChange:
- // continue bisection
- default:
- return errors.Wrapf(err, "failed to verify the header #%d ", newHeader.Height)
- }
-
- if newHeader.Height == c.trustedHeader.Height+1 {
- // TODO: submit evidence here
- return errors.Errorf("adjacent headers (#%d and #%d) that are not matching", lastHeader.Height, newHeader.Height)
- }
-
- pivot := (c.trustedHeader.Height + newHeader.Header.Height) / 2
- pivotHeader, pivotVals, err := c.fetchHeaderAndValsAtHeight(pivot)
- if err != nil {
- return err
- }
-
- // left branch
- {
- err := c.bisection(lastHeader, lastVals, pivotHeader, pivotVals, now)
- if err != nil {
- return errors.Wrapf(err, "bisection of #%d and #%d", lastHeader.Height, pivot)
- }
- }
-
- // right branch
- {
- nextVals, err := c.primary.ValidatorSet(pivot + 1)
- if err != nil {
- return errors.Wrapf(err, "failed to obtain the vals #%d", pivot+1)
- }
- if !bytes.Equal(pivotHeader.NextValidatorsHash, nextVals.Hash()) {
- return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)",
- pivotHeader.NextValidatorsHash,
- nextVals.Hash(),
- pivot)
- }
-
- err = c.updateTrustedHeaderAndVals(pivotHeader, nextVals)
- if err != nil {
- return errors.Wrapf(err, "failed to update trusted state #%d", pivot)
- }
-
- err = c.bisection(pivotHeader, nextVals, newHeader, newVals, now)
- if err != nil {
- return errors.Wrapf(err, "bisection of #%d and #%d", pivot, newHeader.Height)
- }
- }
-
- return nil
- }
-
- func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error {
- if !bytes.Equal(h.NextValidatorsHash, vals.Hash()) {
- return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, vals.Hash())
- }
-
- if err := c.trustedStore.SaveSignedHeader(h); err != nil {
- return errors.Wrap(err, "failed to save trusted header")
- }
- if err := c.trustedStore.SaveValidatorSet(vals, h.Height+1); err != nil {
- return errors.Wrap(err, "failed to save trusted vals")
- }
- c.trustedHeader = h
- c.trustedNextVals = vals
- return nil
- }
-
- func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) {
- h, err := c.primary.SignedHeader(height)
- if err != nil {
- return nil, nil, errors.Wrapf(err, "failed to obtain the header #%d", height)
- }
- vals, err := c.primary.ValidatorSet(height)
- if err != nil {
- return nil, nil, errors.Wrapf(err, "failed to obtain the vals #%d", height)
- }
- return h, vals, nil
- }
-
- func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) error {
- // 1. Pick an alternative provider.
- p := c.alternatives[tmrand.Intn(len(c.alternatives))]
-
- // 2. Fetch the header.
- altHeader, err := p.SignedHeader(h.Height)
- if err != nil {
- return errors.Wrapf(err,
- "failed to obtain header #%d from alternative provider %v", h.Height, p)
- }
-
- // 3. Compare hashes.
- if !bytes.Equal(h.Hash(), altHeader.Hash()) {
- // TODO: One of the providers is lying. Send the evidence to fork
- // accountability server.
- return errors.Errorf(
- "new header hash %X does not match one from alternative provider %X",
- h.Hash(), altHeader.Hash())
- }
-
- return nil
- }
|