Files
tendermint/lite2/client.go
T
Callum WatersandGitHub af37db39b0 lite2: cross-check new header with all witnesses (#4373)
As opposed to checking a random witness, all witnesses provided should be used as a reference against the header provided by the primary node. This increases security (at the tradeoff of speed) but also gives control to the user. The more witnesses provided, the more secure the lite client can be.
2020-02-06 15:36:13 +01:00

1066 lines
33 KiB
Go

package lite
import (
"bytes"
"fmt"
"math/rand"
"sync"
"time"
"github.com/pkg/errors"
"github.com/tendermint/tendermint/libs/log"
tmmath "github.com/tendermint/tendermint/libs/math"
"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
defaultUpdatePeriod = 5 * time.Second
defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour
maxRetryAttempts = 10
)
// Option sets a parameter for the light client.
type Option func(*Client)
// SequentialVerification option configures the light client to sequentially
// check the headers (every header, in ascending height order). 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-adjacent headers. For adjacent headers, sequential
// verification is used.
func SkippingVerification(trustLevel tmmath.Fraction) Option {
return func(c *Client) {
c.verificationMode = skipping
c.trustLevel = trustLevel
}
}
// UpdatePeriod option can be used to change default polling period (5s).
func UpdatePeriod(d time.Duration) Option {
return func(c *Client) {
c.updatePeriod = d
}
}
// RemoveNoLongerTrustedHeadersPeriod option can be used to define how often
// the routine, which cleans up no longer trusted headers (outside of trusting
// period), is run. Default: once a day. When set to zero, the routine won't be
// started.
func RemoveNoLongerTrustedHeadersPeriod(d time.Duration) Option {
return func(c *Client) {
c.removeNoLongerTrustedHeadersPeriod = d
}
}
// ConfirmationFunction option can be used to prompt to confirm an action. For
// example, remove newer headers if the light client is being reset with an
// older header. No confirmation is required by default!
func ConfirmationFunction(fn func(action string) bool) Option {
return func(c *Client) {
c.confirmationFn = fn
}
}
// Logger option can be used to set a logger for the client.
func Logger(l log.Logger) Option {
return func(c *Client) {
c.logger = l
}
}
// 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).
//
// By default, the client will poll the primary provider for new headers every
// 5s (UpdatePeriod). If there are any, it will try to advance the state.
//
// Default verification: SkippingVerification(DefaultTrustLevel)
type Client struct {
chainID string
trustingPeriod time.Duration // see TrustOptions.Period
verificationMode mode
trustLevel tmmath.Fraction
// Mutex for locking during changes of the lite clients providers
providerMutex sync.Mutex
// Primary provider of new headers.
primary provider.Provider
// See Witnesses option
witnesses []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
// See UpdatePeriod option
updatePeriod time.Duration
// See RemoveNoLongerTrustedHeadersPeriod option
removeNoLongerTrustedHeadersPeriod time.Duration
// See ConfirmationFunction option
confirmationFn func(action string) bool
routinesWaitGroup sync.WaitGroup
quit chan struct{}
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).
//
// Witnesses are providers, which will be used for cross-checking the primary
// provider. At least one witness must be given. A witness can become a primary
// iff the current primary is unavailable.
//
// See all Option(s) for the additional configuration.
func NewClient(
chainID string,
trustOptions TrustOptions,
primary provider.Provider,
witnesses []provider.Provider,
trustedStore store.Store,
options ...Option) (*Client, error) {
c := &Client{
chainID: chainID,
trustingPeriod: trustOptions.Period,
verificationMode: skipping,
trustLevel: DefaultTrustLevel,
primary: primary,
witnesses: witnesses,
trustedStore: trustedStore,
updatePeriod: defaultUpdatePeriod,
removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod,
confirmationFn: func(action string) bool { return true },
quit: make(chan struct{}),
logger: log.NewNopLogger(),
}
for _, o := range options {
o(c)
}
// Validate the number of witnesses.
if len(c.witnesses) < 1 {
return nil, errors.New("expected at least one witness")
}
// Verify witnesses are all on the same chain.
for i, w := range witnesses {
if w.ChainID() != chainID {
return nil, errors.Errorf("witness #%d: %v is on another chain %s, expected %s",
i, w, w.ChainID(), chainID)
}
}
// Validate trust level.
if err := ValidateTrustLevel(c.trustLevel); err != nil {
return nil, err
}
if err := c.restoreTrustedHeaderAndNextVals(); err != nil {
return nil, err
}
if c.trustedHeader != nil {
if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil {
return nil, err
}
}
if c.trustedHeader == nil || c.trustedHeader.Height != trustOptions.Height {
if err := c.initializeWithTrustOptions(trustOptions); err != nil {
return nil, err
}
}
return c, nil
}
// Load trustedHeader and trustedNextVals from trustedStore.
func (c *Client) restoreTrustedHeaderAndNextVals() error {
lastHeight, err := c.trustedStore.LastSignedHeaderHeight()
if err != nil {
return errors.Wrap(err, "can't get last trusted header height")
}
if lastHeight > 0 {
trustedHeader, err := c.trustedStore.SignedHeader(lastHeight)
if err != nil {
return errors.Wrap(err, "can't get last trusted header")
}
trustedNextVals, err := c.trustedStore.ValidatorSet(lastHeight + 1)
if err != nil {
return errors.Wrap(err, "can't get last trusted next validators")
}
c.trustedHeader = trustedHeader
c.trustedNextVals = trustedNextVals
c.logger.Debug("Restored trusted header and next vals", lastHeight)
}
return nil
}
// if options.Height:
//
// 1) ahead of trustedHeader.Height => fetch header (same height as
// trustedHeader) from primary provider and check it's hash matches the
// trustedHeader's hash (if not, remove trustedHeader and all the headers
// before)
//
// 2) equals trustedHeader.Height => check options.Hash matches the
// trustedHeader's hash (if not, remove trustedHeader and all the headers
// before)
//
// 3) behind trustedHeader.Height => remove all the headers between
// options.Height and trustedHeader.Height, update trustedHeader, then
// check options.Hash matches the trustedHeader's hash (if not, remove
// trustedHeader and all the headers before)
//
// The intuition here is the user is always right. I.e. if she decides to reset
// the light client with an older header, there must be a reason for it.
func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error {
var primaryHash []byte
switch {
case options.Height > c.trustedHeader.Height:
h, err := c.signedHeaderFromPrimary(c.trustedHeader.Height)
if err != nil {
return err
}
primaryHash = h.Hash()
case options.Height == c.trustedHeader.Height:
primaryHash = options.Hash
case options.Height < c.trustedHeader.Height:
c.logger.Info("Client initialized with old header (trusted is more recent)",
"old", options.Height,
"trusted", c.trustedHeader.Height)
action := fmt.Sprintf(
"Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)",
options.Height, options.Hash,
c.trustedHeader.Height, c.trustedHeader.Hash())
if c.confirmationFn(action) {
// remove all the headers ( options.Height, trustedHeader.Height ]
c.cleanup(options.Height + 1)
c.logger.Info("Rolled back to older header (newer headers were removed)",
"old", options.Height)
} else {
return errors.New("rollback aborted")
}
primaryHash = options.Hash
}
if !bytes.Equal(primaryHash, c.trustedHeader.Hash()) {
c.logger.Info("Prev. trusted header's hash (h1) doesn't match hash from primary provider (h2)",
"h1", c.trustedHeader.Hash(), "h1", primaryHash)
action := fmt.Sprintf(
"Prev. trusted header's hash %X doesn't match hash %X from primary provider. Remove all the stored headers?",
c.trustedHeader.Hash(), primaryHash)
if c.confirmationFn(action) {
err := c.Cleanup()
if err != nil {
return errors.Wrap(err, "failed to cleanup")
}
} else {
return errors.New("refused to remove the stored headers despite hashes mismatch")
}
}
return nil
}
// Fetch trustedHeader and trustedNextVals from primary provider.
func (c *Client) initializeWithTrustOptions(options TrustOptions) error {
// 1) Fetch and verify the header.
h, err := c.signedHeaderFromPrimary(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 err
}
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 vals.
vals, err := c.validatorSetFromPrimary(options.Height)
if err != nil {
return err
}
if !bytes.Equal(h.ValidatorsHash, vals.Hash()) {
return errors.Errorf("expected header's validators (%X) to match those that were supplied (%X)",
h.ValidatorsHash,
vals.Hash(),
)
}
// Ensure that +2/3 of validators signed correctly.
err = vals.VerifyCommit(c.chainID, h.Commit.BlockID, h.Height, h.Commit)
if err != nil {
return errors.Wrap(err, "invalid commit")
}
// 3) Fetch and verify the next vals (verification happens in
// updateTrustedHeaderAndVals).
nextVals, err := c.validatorSetFromPrimary(options.Height + 1)
if err != nil {
return err
}
// 4) Persist both of them and continue.
return c.updateTrustedHeaderAndVals(h, nextVals)
}
// Start starts two processes: 1) auto updating 2) removing outdated headers.
func (c *Client) Start() error {
c.logger.Info("Starting light client")
if c.removeNoLongerTrustedHeadersPeriod > 0 {
c.routinesWaitGroup.Add(1)
go c.removeNoLongerTrustedHeadersRoutine()
}
if c.updatePeriod > 0 {
c.routinesWaitGroup.Add(1)
go c.autoUpdateRoutine()
}
return nil
}
// Stop stops two processes: 1) auto updating 2) removing outdated headers.
// Stop only returns after both of them are finished running. If you wish to
// remove all the data, call Cleanup.
func (c *Client) Stop() {
c.logger.Info("Stopping light client")
close(c.quit)
c.routinesWaitGroup.Wait()
}
// TrustedHeader returns a trusted header at the given height (0 - the latest).
// If a header is missing in trustedStore (e.g. it was skipped during
// bisection), it will be downloaded from primary.
//
// Headers along with validator sets, which can't be trusted anymore, are
// removed once a day (can be changed with RemoveNoLongerTrustedHeadersPeriod
// option).
// .
// height must be >= 0.
//
// It returns an error if:
// - header expired, therefore can't be trusted (ErrOldHeaderExpired);
// - there are some issues with the trusted store, although that should not
// happen normally;
// - negative height is passed;
// - header has not been verified yet
//
// Safe for concurrent use by multiple goroutines.
func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) {
if height < 0 {
return nil, errors.New("negative height")
}
// 1) Get latest height.
latestHeight, err := c.LastTrustedHeight()
if err != nil {
return nil, err
}
if latestHeight == -1 {
return nil, errors.New("no headers exist")
}
if height > latestHeight {
return nil, errors.Errorf("unverified header requested (latest: %d)", latestHeight)
}
if height == 0 {
height = latestHeight
}
// 2) Get header from store.
h, err := c.trustedStore.SignedHeader(height)
switch {
case errors.Is(err, store.ErrSignedHeaderNotFound):
// 2.1) If not found, try to fetch header from primary.
h, err = c.fetchMissingTrustedHeader(height, now)
if err != nil {
return nil, err
}
case err != nil:
return nil, err
}
// 3) Ensure header can still be trusted.
if HeaderExpired(h, c.trustingPeriod, now) {
return nil, ErrOldHeaderExpired{h.Time.Add(c.trustingPeriod), now}
}
return h, nil
}
// TrustedValidatorSet returns a trusted validator set at the given height. If
// a validator set is missing in trustedStore (e.g. the associated header was
// skipped during bisection), it will be downloaded from primary. The second
// return parameter is height validator set corresponds to (useful when you
// pass 0).
//
// height must be >= 0.
//
// Headers along with validator sets, which can't be trusted anymore, are
// removed once a day (can be changed with RemoveNoLongerTrustedHeadersPeriod
// option).
//
// It returns an error if:
// - header signed by that validator set expired (ErrOldHeaderExpired)
// - there are some issues with the trusted store, although that should not
// happen normally;
// - negative height is passed;
// - header signed by that validator set has not been verified yet
//
// Safe for concurrent use by multiple goroutines.
func (c *Client) TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error) {
// Checks height is positive and header (note: height - 1) is not expired.
// Additionally, it fetches validator set from primary if it's missing in
// store.
_, err := c.TrustedHeader(height-1, now)
if err != nil {
return nil, err
}
return c.trustedStore.ValidatorSet(height)
}
// LastTrustedHeight returns a last trusted height. -1 and nil are returned if
// there are no trusted headers.
//
// Safe for concurrent use by multiple goroutines.
func (c *Client) LastTrustedHeight() (int64, error) {
return c.trustedStore.LastSignedHeaderHeight()
}
// FirstTrustedHeight returns a first trusted height. -1 and nil are returned if
// there are no trusted headers.
//
// Safe for concurrent use by multiple goroutines.
func (c *Client) FirstTrustedHeight() (int64, error) {
return c.trustedStore.FirstSignedHeaderHeight()
}
// ChainID returns the chain ID the light client was configured with.
//
// Safe for concurrent use by multiple goroutines.
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.
// If the header is not found by the primary provider,
// provider.ErrSignedHeaderNotFound error is returned.
func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) {
c.logger.Info("VerifyHeaderAtHeight", "height", height)
if c.trustedHeader.Height >= height {
return nil, errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height)
}
// Request the header and the vals.
newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height)
if err != nil {
return nil, err
}
return newHeader, 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 details.
// https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md
//
// If the trusted header is more recent than one here, an error is returned.
//
// If, at any moment, SignedHeader or ValidatorSet are not found by the primary
// provider, provider.ErrSignedHeaderNotFound /
// provider.ErrValidatorSetNotFound error is returned.
func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
c.logger.Info("VerifyHeader", "height", newHeader.Hash(), "newVals", fmt.Sprintf("%X", newVals.Hash()))
if c.trustedHeader.Height >= newHeader.Height {
return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height)
}
if err := c.compareNewHeaderWithWitnesses(newHeader); err != nil {
c.logger.Error("Error when comparing new header with one from a witness", "err", err)
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.validatorSetFromPrimary(newHeader.Height + 1)
if err != nil {
return err
}
return c.updateTrustedHeaderAndVals(newHeader, nextVals)
}
// Primary returns the primary provider.
//
// NOTE: provider may be not safe for concurrent access.
func (c *Client) Primary() provider.Provider {
c.providerMutex.Lock()
defer c.providerMutex.Unlock()
return c.primary
}
// Witnesses returns the witness providers.
//
// NOTE: providers may be not safe for concurrent access.
func (c *Client) Witnesses() []provider.Provider {
c.providerMutex.Lock()
defer c.providerMutex.Unlock()
return c.witnesses
}
// Cleanup removes all the data (headers and validator sets) stored. Note: the
// client must be stopped at this point.
func (c *Client) Cleanup() error {
c.logger.Info("Removing all the data")
return c.cleanup(0)
}
// cleanup deletes all headers & validator sets between +stopHeight+ and latest height included
func (c *Client) cleanup(stopHeight int64) error {
// 1) Get the oldest height.
oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight()
if err != nil {
return errors.Wrap(err, "can't get first trusted height")
}
// 2) Get the latest height.
latestHeight, err := c.trustedStore.LastSignedHeaderHeight()
if err != nil {
return errors.Wrap(err, "can't get last trusted height")
}
// 3) Remove all headers and validator sets.
if stopHeight < oldestHeight {
stopHeight = oldestHeight
}
for height := stopHeight; height <= latestHeight; height++ {
err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height)
if err != nil {
c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height)
continue
}
}
c.trustedHeader = nil
c.trustedNextVals = nil
err = c.restoreTrustedHeaderAndNextVals()
if err != nil {
return err
}
return nil
}
// see VerifyHeader
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.signedHeaderFromPrimary(height)
if err != nil {
return errors.Wrapf(err, "failed to obtain the header #%d", height)
}
c.logger.Debug("Verify newHeader against lastHeader",
"lastHeight", c.trustedHeader.Height,
"lastHash", c.trustedHeader.Hash(),
"newHeight", interimHeader.Height,
"newHash", interimHeader.Hash())
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.validatorSetFromPrimary(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)
}
// see VerifyHeader
func (c *Client) bisection(
lastHeader *types.SignedHeader,
lastVals *types.ValidatorSet,
newHeader *types.SignedHeader,
newVals *types.ValidatorSet,
now time.Time) error {
c.logger.Debug("Verify newHeader against lastHeader",
"lastHeight", lastHeader.Height,
"lastHash", lastHeader.Hash(),
"newHeight", newHeader.Height,
"newHash", newHeader.Hash())
err := Verify(c.chainID, lastHeader, lastVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel)
switch err.(type) {
case nil:
return nil
case ErrNewValSetCantBeTrusted:
// continue bisection
default:
return errors.Wrapf(err, "failed to verify the header #%d", 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.validatorSetFromPrimary(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
}
// persist header and next validators to trustedStore.
func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error {
if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) {
return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, nextVals.Hash())
}
if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil {
return errors.Wrap(err, "failed to save trusted header")
}
c.trustedHeader = h
c.trustedNextVals = nextVals
return nil
}
// fetch header and validators for the given height from primary provider.
func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) {
h, err := c.signedHeaderFromPrimary(height)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to obtain the header #%d", height)
}
vals, err := c.validatorSetFromPrimary(height)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to obtain the vals #%d", height)
}
return h, vals, nil
}
// fetchMissingTrustedHeader finds the closest height after the
// requested height and does backwards verification.
func (c *Client) fetchMissingTrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) {
c.logger.Info("Fetching missing header", "height", height)
closestHeader, err := c.trustedStore.SignedHeaderAfter(height)
if err != nil {
return nil, errors.Wrapf(err, "can't get signed header after %d", height)
}
// Perform backwards verification from closestHeader to header at the given
// height.
h, err := c.backwards(height, closestHeader, now)
if err != nil {
return nil, err
}
// Fetch next validator set from primary and persist it.
nextVals, err := c.validatorSetFromPrimary(height + 1)
if err != nil {
return nil, errors.Wrapf(err, "failed to obtain the vals #%d", height)
}
if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) {
return nil, errors.Errorf("expected next validator's hash %X, but got %X",
h.NextValidatorsHash, nextVals.Hash())
}
if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil {
return nil, errors.Wrap(err, "failed to save trusted header")
}
return h, nil
}
// Backwards verification (see VerifyHeaderBackwards func in the spec)
func (c *Client) backwards(toHeight int64, fromHeader *types.SignedHeader, now time.Time) (*types.SignedHeader, error) {
var (
trustedHeader = fromHeader
untrustedHeader *types.SignedHeader
err error
)
for i := trustedHeader.Height - 1; i >= toHeight; i-- {
untrustedHeader, err = c.signedHeaderFromPrimary(i)
if err != nil {
return nil, errors.Wrapf(err, "failed to obtain the header #%d", i)
}
if err := untrustedHeader.ValidateBasic(c.chainID); err != nil {
return nil, errors.Wrap(err, "untrustedHeader.ValidateBasic failed")
}
if !untrustedHeader.Time.Before(trustedHeader.Time) {
return nil, errors.Errorf("expected older header time %v to be before newer header time %v",
untrustedHeader.Time,
trustedHeader.Time)
}
if HeaderExpired(untrustedHeader, c.trustingPeriod, now) {
return nil, ErrOldHeaderExpired{untrustedHeader.Time.Add(c.trustingPeriod), now}
}
if !bytes.Equal(untrustedHeader.Hash(), trustedHeader.LastBlockID.Hash) {
return nil, errors.Errorf("older header hash %X does not match trusted header's last block %X",
untrustedHeader.Hash(),
trustedHeader.LastBlockID.Hash)
}
trustedHeader = untrustedHeader
}
return trustedHeader, nil
}
// compare header with all witnesses provided.
func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error {
c.providerMutex.Lock()
defer c.providerMutex.Unlock()
// 0. Check witnesses exist
if len(c.witnesses) == 0 {
return errors.New("could not find any witnesses")
}
// 1. Loop through all witnesses.
for _, witness := range c.witnesses {
// 2. Fetch the header.
altH, err := witness.SignedHeader(h.Height)
if err != nil {
return errors.Wrapf(err,
"failed to obtain header #%d from the witness %v", h.Height, witness)
}
// 3. Compare hashes.
if !bytes.Equal(h.Hash(), altH.Hash()) {
// TODO: One of the providers is lying. Send the evidence to fork
// accountability server.
return errors.Errorf(
"header hash %X does not match one %X from the witness %v",
h.Hash(), altH.Hash(), witness)
}
}
return nil
}
func (c *Client) removeNoLongerTrustedHeadersRoutine() {
defer c.routinesWaitGroup.Done()
ticker := time.NewTicker(c.removeNoLongerTrustedHeadersPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.RemoveNoLongerTrustedHeaders(time.Now())
case <-c.quit:
return
}
}
}
// RemoveNoLongerTrustedHeaders removes no longer trusted headers (due to
// expiration).
//
// Exposed for testing.
func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) {
// 1) Get the oldest height.
oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight()
if err != nil {
c.logger.Error("can't get first trusted height", "err", err)
return
}
// 2) Get the latest height.
latestHeight, err := c.LastTrustedHeight()
if err != nil {
c.logger.Error("can't get last trusted height", "err", err)
return
}
// 3) Remove all headers that are outside of the trusting period.
for height := oldestHeight; height <= latestHeight; height++ {
h, err := c.trustedStore.SignedHeader(height)
if err != nil {
c.logger.Error("can't get a trusted header", "err", err, "height", height)
continue
}
// Stop if the header is within the trusting period.
if !HeaderExpired(h, c.trustingPeriod, now) {
break
}
err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height)
if err != nil {
c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height)
continue
}
}
}
func (c *Client) autoUpdateRoutine() {
defer c.routinesWaitGroup.Done()
ticker := time.NewTicker(c.updatePeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
err := c.Update(time.Now())
if err != nil {
c.logger.Error("Error during auto update", "err", err)
}
case <-c.quit:
return
}
}
}
// Update attempts to advance the state making exponential steps (note:
// when SequentialVerification is being used, the client will still be
// downloading all intermediate headers).
//
// Exposed for testing.
func (c *Client) Update(now time.Time) error {
lastTrustedHeight, err := c.LastTrustedHeight()
if err != nil {
return errors.Wrap(err, "can't get last trusted height")
}
if lastTrustedHeight == -1 {
// no headers yet => wait
return nil
}
latestHeader, latestVals, err := c.fetchHeaderAndValsAtHeight(0)
if err != nil {
return errors.Wrapf(err, "can't get latest header and vals")
}
if latestHeader.Height > lastTrustedHeight {
err = c.VerifyHeader(latestHeader, latestVals, now)
if err != nil {
return err
}
c.logger.Info("Advanced to new state", "height", latestHeader.Height, "hash", latestHeader.Hash())
}
return nil
}
// replaceProvider takes the first alternative provider and promotes it as the primary provider
func (c *Client) replacePrimaryProvider() error {
c.providerMutex.Lock()
defer c.providerMutex.Unlock()
if len(c.witnesses) == 0 {
return errors.Errorf("no witnesses left")
}
c.primary = c.witnesses[0]
c.witnesses = c.witnesses[1:]
c.logger.Info("New primary", "p", c.primary)
return nil
}
// signedHeaderFromPrimary retrieves the SignedHeader from the primary provider at the specified height.
// Handles dropout by the primary provider by swapping with an alternative provider
func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, error) {
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
c.providerMutex.Lock()
h, err := c.primary.SignedHeader(height)
c.providerMutex.Unlock()
if err == nil {
// sanity check
if height > 0 && h.Height != height {
return nil, errors.Errorf("expected %d height, got %d", height, h.Height)
}
return h, nil
}
if err == provider.ErrSignedHeaderNotFound {
return nil, err
}
time.Sleep(backoffTimeout(attempt))
}
c.logger.Info("Primary is unavailable. Replacing with the first witness")
err := c.replacePrimaryProvider()
if err != nil {
return nil, err
}
return c.signedHeaderFromPrimary(height)
}
// validatorSetFromPrimary retrieves the ValidatorSet from the primary provider at the specified height.
// Handles dropout by the primary provider after 5 attempts by replacing it with an alternative provider
func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, error) {
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
c.providerMutex.Lock()
vals, err := c.primary.ValidatorSet(height)
c.providerMutex.Unlock()
if err == nil || err == provider.ErrValidatorSetNotFound {
return vals, err
}
time.Sleep(backoffTimeout(attempt))
}
c.logger.Info("Primary is unavailable. Replacing with the first witness")
err := c.replacePrimaryProvider()
if err != nil {
return nil, err
}
return c.validatorSetFromPrimary(height)
}
// exponential backoff (with jitter)
// 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
func backoffTimeout(attempt int) time.Duration {
return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
}