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" ) type mode byte const ( sequential mode = iota + 1 skipping defaultUpdatePeriod = 5 * time.Second defaultPruningSize = 1000 defaultMaxRetryAttempts = 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 } } // PruningSize option sets the maximum amount of headers & validator set pairs // that the light client stores. When Prune() is run, all headers (along with // the associated validator sets) that are earlier than the h amount of headers // will be removed from the store. Default: 1000. A pruning size of 0 will not // prune the lite client at all. func PruningSize(h uint16) Option { return func(c *Client) { c.pruningSize = h } } // 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 } } // MaxRetryAttempts option can be used to set max attempts before replacing // primary with a witness. func MaxRetryAttempts(max uint16) Option { return func(c *Client) { c.maxRetryAttempts = max } } // 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 maxRetryAttempts uint16 // see MaxRetryAttempts option // 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). latestTrustedHeader *types.SignedHeader // Highest validator set from the store (height=H). latestTrustedVals *types.ValidatorSet // See UpdatePeriod option updatePeriod time.Duration // See RemoveNoLongerTrustedHeadersPeriod option pruningSize uint16 // 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) { if err := trustOptions.ValidateBasic(); err != nil { return nil, errors.Wrap(err, "invalid TrustOptions") } c, err := NewClientFromTrustedStore(chainID, trustOptions.Period, primary, witnesses, trustedStore, options...) if err != nil { return nil, err } if c.latestTrustedHeader != nil { if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil { return nil, err } } if c.latestTrustedHeader == nil || c.latestTrustedHeader.Height < trustOptions.Height { if err := c.initializeWithTrustOptions(trustOptions); err != nil { return nil, err } } return c, err } // NewClientFromTrustedStore initializes existing client from the trusted store. // // See NewClient func NewClientFromTrustedStore( chainID string, trustingPeriod time.Duration, primary provider.Provider, witnesses []provider.Provider, trustedStore store.Store, options ...Option) (*Client, error) { c := &Client{ chainID: chainID, trustingPeriod: trustingPeriod, verificationMode: skipping, trustLevel: DefaultTrustLevel, maxRetryAttempts: defaultMaxRetryAttempts, primary: primary, witnesses: witnesses, trustedStore: trustedStore, updatePeriod: defaultUpdatePeriod, pruningSize: defaultPruningSize, 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.restoreTrustedHeaderAndVals(); err != nil { return nil, err } return c, nil } // restoreTrustedHeaderAndVals loads trustedHeader and trustedVals from // trustedStore. func (c *Client) restoreTrustedHeaderAndVals() 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") } trustedVals, err := c.trustedStore.ValidatorSet(lastHeight) if err != nil { return errors.Wrap(err, "can't get last trusted validators") } c.latestTrustedHeader = trustedHeader c.latestTrustedVals = trustedVals c.logger.Debug("Restored trusted header and vals", "height", 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.latestTrustedHeader.Height: h, err := c.signedHeaderFromPrimary(c.latestTrustedHeader.Height) if err != nil { return err } primaryHash = h.Hash() case options.Height == c.latestTrustedHeader.Height: primaryHash = options.Hash case options.Height < c.latestTrustedHeader.Height: c.logger.Info("Client initialized with old header (trusted is more recent)", "old", options.Height, "trustedHeight", c.latestTrustedHeader.Height, "trustedHash", hash2str(c.latestTrustedHeader.Hash())) action := fmt.Sprintf( "Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)", options.Height, options.Hash, c.latestTrustedHeader.Height, c.latestTrustedHeader.Hash()) if c.confirmationFn(action) { // remove all the headers (options.Height, trustedHeader.Height] err := c.cleanupAfter(options.Height) if err != nil { return errors.Wrapf(err, "cleanupAfter(%d)", options.Height) } c.logger.Info("Rolled back to older header (newer headers were removed)", "old", options.Height) } else { return nil } primaryHash = options.Hash } if !bytes.Equal(primaryHash, c.latestTrustedHeader.Hash()) { c.logger.Info("Prev. trusted header's hash (h1) doesn't match hash from primary provider (h2)", "h1", hash2str(c.latestTrustedHeader.Hash()), "h2", hash2str(primaryHash)) action := fmt.Sprintf( "Prev. trusted header's hash %X doesn't match hash %X from primary provider. Remove all the stored headers?", c.latestTrustedHeader.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 } // initializeWithTrustOptions fetches the weakly-trusted header and vals from // primary provider. The header is cross-checked with witnesses for additional // security. 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. // - h.Time is not being checked against time.Now() because we don't // want to add yet another argument to NewClient* functions. 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()) } err = c.compareNewHeaderWithWitnesses(h) if err != nil { return err } // 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) Persist both of them and continue. return c.updateTrustedHeaderAndVals(h, vals) } // Start starts two processes: 1) auto updating 2) removing outdated headers. func (c *Client) Start() error { c.logger.Info("Starting light client") 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). // // 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: // - there are some issues with the trusted store, although that should not // happen normally; // - negative height is passed; // - header has not been verified yet and is therefore not in the store // // Safe for concurrent use by multiple goroutines. func (c *Client) TrustedHeader(height int64) (*types.SignedHeader, error) { height, err := c.compareWithLatestHeight(height) if err != nil { return nil, err } return c.trustedStore.SignedHeader(height) } // TrustedValidatorSet returns a trusted validator set at the given height (0 - // latest). The second return parameter is the height used (useful if 0 was // passed; otherwise can be ignored). // // height must be >= 0. // // Headers along with validator sets are // removed once a day (can be changed with RemoveNoLongerTrustedHeadersPeriod // option). // // Function returns an error if: // - 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) (valSet *types.ValidatorSet, heightUsed int64, err error) { heightUsed, err = c.compareWithLatestHeight(height) if err != nil { return nil, heightUsed, err } valSet, err = c.trustedStore.ValidatorSet(heightUsed) if err != nil { return nil, heightUsed, err } return valSet, heightUsed, err } func (c *Client) compareWithLatestHeight(height int64) (int64, error) { latestHeight, err := c.LastTrustedHeight() if err != nil { return 0, errors.Wrap(err, "can't get last trusted height") } if latestHeight == -1 { return 0, errors.New("no headers exist") } switch { case height > latestHeight: return 0, errors.Errorf("unverified header/valset requested (latest: %d)", latestHeight) case height == 0: return latestHeight, nil case height < 0: return 0, errors.New("negative height") } return height, nil } // 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 header and validators at the given height // and calls VerifyHeader. It returns header immediately if such exists in // trustedStore (no verification is needed). // // It returns provider.ErrSignedHeaderNotFound if header is not found by // primary. // It returns ErrOldHeaderExpired if header expired. func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { if height <= 0 { return nil, errors.New("negative or zero height") } // Check if header already verified. h, err := c.TrustedHeader(height) if err == nil { c.logger.Info("Header has already been verified", "height", height, "hash", hash2str(h.Hash())) // Return already trusted header return h, nil } // 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. It returns // immediately if newHeader exists in trustedStore (no verification is // needed). // // 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 // // It returns ErrOldHeaderExpired if newHeader expired. // // 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 { if newHeader.Height <= 0 { return errors.New("negative or zero height") } // Check if newHeader already verified. h, err := c.TrustedHeader(newHeader.Height) if err == nil { // Make sure it's the same header. if !bytes.Equal(h.Hash(), newHeader.Hash()) { return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) } c.logger.Info("Header has already been verified", "height", newHeader.Height, "hash", hash2str(newHeader.Hash())) return nil } return c.verifyHeader(newHeader, newVals, now) } func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { c.logger.Info("VerifyHeader", "height", newHeader.Height, "hash", hash2str(newHeader.Hash()), "vals", hash2str(newVals.Hash())) var err error // 1) If going forward, perform either bisection or sequential verification if newHeader.Height >= c.latestTrustedHeader.Height { switch c.verificationMode { case sequential: err = c.sequence(c.latestTrustedHeader, newHeader, newVals, now) case skipping: err = c.bisection(c.latestTrustedHeader, c.latestTrustedVals, newHeader, newVals, now) default: panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) } } else { // 2) Otherwise, perform backwards verification // Find the closest trusted header after newHeader.Height var closestHeader *types.SignedHeader closestHeader, err = c.trustedStore.SignedHeaderAfter(newHeader.Height) if err != nil { return errors.Wrapf(err, "can't get signed header after height %d", newHeader.Height) } err = c.backwards(closestHeader, newHeader, now) } if err != nil { c.logger.Error("Can't verify", "err", err) return err } if err := c.compareNewHeaderWithWitnesses(newHeader); err != nil { c.logger.Error("Error when comparing new header with witnesses", "err", err) return err } return c.updateTrustedHeaderAndVals(newHeader, newVals) } // 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") c.latestTrustedHeader = nil c.latestTrustedVals = nil return c.trustedStore.Prune(0) } // cleanupAfter deletes all headers & validator sets after +height+. It also // resets latestTrustedHeader to the latest header. func (c *Client) cleanupAfter(height int64) error { nextHeight := height for { h, err := c.trustedStore.SignedHeaderAfter(nextHeight) if err == store.ErrSignedHeaderNotFound { break } else if err != nil { return errors.Wrapf(err, "failed to get header after %d", nextHeight) } err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(h.Height) if err != nil { c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", h.Height) } nextHeight = h.Height } c.latestTrustedHeader = nil c.latestTrustedVals = nil err := c.restoreTrustedHeaderAndVals() if err != nil { return err } return nil } // see VerifyHeader func (c *Client) sequence( initiallyTrustedHeader *types.SignedHeader, newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { var ( trustedHeader = initiallyTrustedHeader interimHeader *types.SignedHeader interimVals *types.ValidatorSet err error ) for height := initiallyTrustedHeader.Height + 1; height <= newHeader.Height; height++ { // 1) Fetch interim headers and vals if needed. if height == newHeader.Height { // last header interimHeader, interimVals = newHeader, newVals } else { // intermediate headers interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(height) if err != nil { return errors.Wrapf(err, "failed to obtain the header #%d", height) } } // 2) Verify them c.logger.Debug("Verify newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", interimHeader.Height, "newHash", hash2str(interimHeader.Hash())) err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, interimVals, c.trustingPeriod, now) if err != nil { return errors.Wrapf(err, "failed to verify the header #%d", height) } // 3) Update trustedHeader trustedHeader = interimHeader } return nil } // see VerifyHeader func (c *Client) bisection( initiallyTrustedHeader *types.SignedHeader, initiallyTrustedVals *types.ValidatorSet, newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { var ( trustedHeader = initiallyTrustedHeader trustedVals = initiallyTrustedVals interimHeader = newHeader interimVals = newVals ) for { c.logger.Debug("Verify newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", interimHeader.Height, "newHash", hash2str(interimHeader.Hash())) err := Verify(c.chainID, trustedHeader, trustedVals, interimHeader, interimVals, c.trustingPeriod, now, c.trustLevel) switch err.(type) { case nil: if interimHeader.Height == newHeader.Height { return nil } // Update the lower bound to the previous upper bound trustedHeader, trustedVals = interimHeader, interimVals // Update the upper bound to the untrustedHeader interimHeader, interimVals = newHeader, newVals case ErrNewValSetCantBeTrusted: pivotHeight := (interimHeader.Height + trustedHeader.Height) / 2 interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(pivotHeight) if err != nil { return err } default: return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) } } } func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { return errors.Errorf("expected validator's hash %X, but got %X", h.ValidatorsHash, vals.Hash()) } if err := c.trustedStore.SaveSignedHeaderAndValidatorSet(h, vals); err != nil { return errors.Wrap(err, "failed to save trusted header") } if c.pruningSize > 0 { if err := c.trustedStore.Prune(c.pruningSize); err != nil { return errors.Wrap(err, "prune") } } if c.latestTrustedHeader == nil || h.Height > c.latestTrustedHeader.Height { c.latestTrustedHeader = h c.latestTrustedVals = vals } return nil } // fetch header and validators for the given height (0 - latest) 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 } // Backwards verification (see VerifyHeaderBackwards func in the spec) func (c *Client) backwards( initiallyTrustedHeader *types.SignedHeader, newHeader *types.SignedHeader, now time.Time) error { if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { return ErrOldHeaderExpired{initiallyTrustedHeader.Time.Add(c.trustingPeriod), now} } var ( trustedHeader = initiallyTrustedHeader interimHeader *types.SignedHeader err error ) for trustedHeader.Height > newHeader.Height { interimHeader, err = c.signedHeaderFromPrimary(trustedHeader.Height - 1) if err != nil { return errors.Wrapf(err, "failed to obtain the header at height #%d", trustedHeader.Height-1) } if err := interimHeader.ValidateBasic(c.chainID); err != nil { return errors.Wrap(err, "untrustedHeader.ValidateBasic failed") } if !interimHeader.Time.Before(trustedHeader.Time) { return errors.Errorf("expected older header time %v to be before newer header time %v", interimHeader.Time, trustedHeader.Time) } if !bytes.Equal(interimHeader.Hash(), trustedHeader.LastBlockID.Hash) { return errors.Errorf("older header hash %X does not match trusted header's last block %X", interimHeader.Hash(), trustedHeader.LastBlockID.Hash) } trustedHeader = interimHeader } // Initially trusted header might have expired at this point. if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { return ErrOldHeaderExpired{initiallyTrustedHeader.Time.Add(c.trustingPeriod), now} } return nil } // compare header with all witnesses provided. func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { c.providerMutex.Lock() defer c.providerMutex.Unlock() // 1. Make sure AT LEAST ONE witness returns the same header. headerMatched := false witnessesToRemove := make([]int, 0) for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { if len(c.witnesses) == 0 { return errors.New("could not find any witnesses. please reset the light client") } for i, witness := range c.witnesses { altH, err := witness.SignedHeader(h.Height) if err != nil { c.logger.Error("Failed to get a header from witness", "height", h.Height, "witness", witness) continue } if err = altH.ValidateBasic(c.chainID); err != nil { c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) witnessesToRemove = append(witnessesToRemove, i) continue } if !bytes.Equal(h.Hash(), altH.Hash()) { if err = c.latestTrustedVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, altH.Height, altH.Commit, c.trustLevel); err != nil { c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) witnessesToRemove = append(witnessesToRemove, i) continue } // TODO: send the diverged headers to primary && all witnesses return errors.Errorf( "header hash %X does not match one %X from the witness %v", h.Hash(), altH.Hash(), witness) } headerMatched = true } for _, idx := range witnessesToRemove { c.removeWitness(idx) } witnessesToRemove = make([]int, 0) if headerMatched { return nil } // 2. Otherwise, sleep time.Sleep(backoffTimeout(attempt)) } return errors.New("awaiting response from all witnesses exceeded dropout time") } // NOTE: requires a providerMutex locked. func (c *Client) removeWitness(idx int) { switch len(c.witnesses) { case 0: panic(fmt.Sprintf("wanted to remove %d element from empty witnesses slice", idx)) case 1: c.witnesses = make([]provider.Provider, 0) default: c.witnesses[idx] = c.witnesses[len(c.witnesses)-1] c.witnesses = c.witnesses[:len(c.witnesses)-1] } } 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", hash2str(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) <= 1 { return errors.Errorf("only one witness left. please reset the light client") } 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 := uint16(1); attempt <= c.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 := uint16(1); attempt <= c.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 uint16) time.Duration { return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond } func hash2str(hash []byte) string { return fmt.Sprintf("%X", hash) }