package light import ( "bytes" "errors" "fmt" "math/rand" "time" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" tmsync "github.com/tendermint/tendermint/libs/sync" "github.com/tendermint/tendermint/light/provider" "github.com/tendermint/tendermint/light/store" "github.com/tendermint/tendermint/types" ) type mode byte const ( sequential mode = iota + 1 skipping defaultPruningSize = 1000 defaultMaxRetryAttempts = 10 // For bisection, when using the cache of headers from the previous batch, // they will always be at a height greater than 1/2 (normal bisection) so to // find something in between the range, 9/16 is used. bisectionNumerator = 9 bisectionDenominator = 16 // 10s should cover most of the clients. // References: // - http://vancouver-webpages.com/time/web.html // - https://blog.codinghorror.com/keeping-time-on-the-pc/ defaultMaxClockDrift = 10 * time.Second ) // 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 } } // 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 light 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 } } // MaxClockDrift defines how much new (untrusted) header's Time can drift into // the future. Default: 10s. func MaxClockDrift(d time.Duration) Option { return func(c *Client) { c.maxClockDrift = d } } // 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 maxRetryAttempts uint16 // see MaxRetryAttempts option maxClockDrift time.Duration // Mutex for locking during changes of the light clients providers providerMutex tmsync.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 RemoveNoLongerTrustedHeadersPeriod option pruningSize uint16 // See ConfirmationFunction option confirmationFn func(action string) bool 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 when skipping verification is // used (default). 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, fmt.Errorf("invalid TrustOptions: %w", err) } c, err := NewClientFromTrustedStore(chainID, trustOptions.Period, primary, witnesses, trustedStore, options...) if err != nil { return nil, err } if c.latestTrustedHeader != nil { c.logger.Info("Checking trusted header using options") if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil { return nil, err } } if c.latestTrustedHeader == nil || c.latestTrustedHeader.Height < trustOptions.Height { c.logger.Info("Downloading trusted header using options") 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, maxClockDrift: defaultMaxClockDrift, primary: primary, witnesses: witnesses, trustedStore: trustedStore, 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 && c.verificationMode == skipping { return nil, errNoWitnesses{} } // Verify witnesses are all on the same chain. for i, w := range witnesses { if w.ChainID() != chainID { return nil, fmt.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 fmt.Errorf("can't get last trusted header height: %w", err) } if lastHeight > 0 { trustedHeader, err := c.trustedStore.SignedHeader(lastHeight) if err != nil { return fmt.Errorf("can't get last trusted header: %w", err) } trustedVals, err := c.trustedStore.ValidatorSet(lastHeight) if err != nil { return fmt.Errorf("can't get last trusted validators: %w", err) } c.latestTrustedHeader = trustedHeader c.latestTrustedVals = trustedVals c.logger.Info("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 fmt.Errorf("cleanupAfter(%d): %w", options.Height, err) } 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 fmt.Errorf("failed to cleanup: %w", err) } } 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. 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 fmt.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 fmt.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.VerifyCommitLight(c.chainID, h.Commit.BlockID, h.Height, h.Commit) if err != nil { return fmt.Errorf("invalid commit: %w", err) } // 3) Persist both of them and continue. return c.updateTrustedHeaderAndVals(h, vals) } // 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, fmt.Errorf("can't get last trusted height: %w", err) } if latestHeight == -1 { return 0, errors.New("no headers exist") } switch { case height > latestHeight: return 0, fmt.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 } // 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). // // height must be > 0. // // It returns provider.ErrSignedHeaderNotFound if header is not found by // primary. // // It will replace the primary provider if an error from a request to the provider occurs 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.signedHeaderAndValSetFromPrimary(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). Else it performs one of the two types of verification: // // 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. Intermediate headers are not saved to database. // // 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. // Intermediate headers are not saved to database. // https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md // // If the header, which is older than the currently trusted header, is // requested and the light client does not have it, VerifyHeader will perform: // a) bisection verification if nearest trusted header is found & not expired // b) backwards verification in all other cases // // It returns ErrOldHeaderExpired if the latest trusted header expired. // // If the primary provides an invalid header (ErrInvalidHeader), it is rejected // and replaced by another provider until all are exhausted. // // 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 fmt.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.bisectionAgainstPrimary(c.latestTrustedHeader, c.latestTrustedVals, newHeader, newVals, now) default: panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) } } else { // 2) If verifying before the first trusted header, perform backwards // verification. var ( closestHeader *types.SignedHeader firstHeaderHeight int64 ) firstHeaderHeight, err = c.FirstTrustedHeight() if err != nil { return fmt.Errorf("can't get first header height: %w", err) } if newHeader.Height < firstHeaderHeight { closestHeader, err = c.TrustedHeader(firstHeaderHeight) if err != nil { return fmt.Errorf("can't get first signed header: %w", err) } if HeaderExpired(closestHeader, c.trustingPeriod, now) { closestHeader = c.latestTrustedHeader } err = c.backwards(closestHeader, newHeader, now) } else { // 3) OR if between trusted headers where the nearest has not expired, // perform bisection verification, else backwards. closestHeader, err = c.trustedStore.SignedHeaderBefore(newHeader.Height) if err != nil { return fmt.Errorf("can't get signed header before height %d: %w", newHeader.Height, err) } var closestValidatorSet *types.ValidatorSet if c.verificationMode == sequential || HeaderExpired(closestHeader, c.trustingPeriod, now) { err = c.backwards(c.latestTrustedHeader, newHeader, now) } else { closestValidatorSet, _, err = c.TrustedValidatorSet(closestHeader.Height) if err != nil { return fmt.Errorf("can't get validator set at height %d: %w", closestHeader.Height, err) } err = c.bisectionAgainstPrimary(closestHeader, closestValidatorSet, newHeader, newVals, now) } } } if err != nil { c.logger.Error("Can't verify", "err", err) return err } // 4) Once verified, save and return return c.updateTrustedHeaderAndVals(newHeader, newVals) } // 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.signedHeaderAndValSetFromPrimary(height) if err != nil { return ErrVerificationFailed{From: trustedHeader.Height, To: height, Reason: err} } } // 2) Verify them c.logger.Debug("Verify adjacent 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, c.maxClockDrift) if err != nil { err := ErrVerificationFailed{From: trustedHeader.Height, To: interimHeader.Height, Reason: err} switch errors.Unwrap(err).(type) { case ErrInvalidHeader: // If the target header is invalid, return immediately. if err.To == newHeader.Height { c.logger.Debug("Target header is invalid", "err", err) return err } // If some intermediate header is invalid, replace the primary and try // again. c.logger.Error("primary sent invalid header -> replacing", "err", err) replaceErr := c.replacePrimaryProvider() if replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) // return original error return err } replacementHeader, replacementVals, fErr := c.signedHeaderAndValSetFromPrimary(newHeader.Height) if fErr != nil { c.logger.Error("Can't fetch header/vals from primary", "err", fErr) // return original error return err } if !bytes.Equal(replacementHeader.Hash(), newHeader.Hash()) || !bytes.Equal(replacementVals.Hash(), newVals.Hash()) { c.logger.Error("Replacement provider has a different header/vals", "newHash", newHeader.Hash(), "newVals", newVals.Hash(), "replHash", replacementHeader.Hash(), "replVals", replacementVals.Hash()) // return original error return err } // attempt to verify header again height-- continue default: return err } } // 3) Update trustedHeader trustedHeader = interimHeader } return nil } // see VerifyHeader // // Bisection finds the middle header between a trusted and new header, // reiterating the action until it verifies a header. A cache of headers // requested from source is kept such that when a verification is made, and the // light client tries again to verify the new header in the middle, the light // client does not need to ask for all the same headers again. func (c *Client) bisection( source provider.Provider, initiallyTrustedHeader *types.SignedHeader, initiallyTrustedVals *types.ValidatorSet, newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { type headerSet struct { sh *types.SignedHeader valSet *types.ValidatorSet } var ( headerCache = []headerSet{{newHeader, newVals}} depth = 0 trustedHeader = initiallyTrustedHeader trustedVals = initiallyTrustedVals ) for { c.logger.Debug("Verify non-adjacent newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", headerCache[depth].sh.Height, "newHash", hash2str(headerCache[depth].sh.Hash())) err := Verify(c.chainID, trustedHeader, trustedVals, headerCache[depth].sh, headerCache[depth].valSet, c.trustingPeriod, now, c.maxClockDrift, c.trustLevel) switch err.(type) { case nil: // Have we verified the last header if depth == 0 { return nil } // If not, update the lower bound to the previous upper bound trustedHeader, trustedVals = headerCache[depth].sh, headerCache[depth].valSet // Remove the untrusted header at the lower bound in the header cache - it's no longer useful headerCache = headerCache[:depth] // Reset the cache depth so that we start from the upper bound again depth = 0 case ErrNewValSetCantBeTrusted: // do add another header to the end of the cache if depth == len(headerCache)-1 { pivotHeight := trustedHeader.Height + (headerCache[depth].sh.Height-trustedHeader. Height)*bisectionNumerator/bisectionDenominator interimHeader, interimVals, err := c.signedHeaderAndValSetFrom(pivotHeight, source) if err != nil { return ErrVerificationFailed{From: trustedHeader.Height, To: pivotHeight, Reason: err} } headerCache = append(headerCache, headerSet{interimHeader, interimVals}) } depth++ default: return ErrVerificationFailed{From: trustedHeader.Height, To: headerCache[depth].sh.Height, Reason: err} } } } // bisectionAgainstPrimary does bisection plus it compares new header with // witnesses and replaces primary if it does not respond after // MaxRetryAttempts. func (c *Client) bisectionAgainstPrimary( initiallyTrustedHeader *types.SignedHeader, initiallyTrustedVals *types.ValidatorSet, newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { err := c.bisection(c.primary, initiallyTrustedHeader, initiallyTrustedVals, newHeader, newVals, now) switch errors.Unwrap(err).(type) { case ErrInvalidHeader: // If the target header is invalid, return immediately. invalidHeaderHeight := err.(ErrVerificationFailed).To if invalidHeaderHeight == newHeader.Height { c.logger.Debug("Target header is invalid", "err", err) return err } // If some intermediate header is invalid, replace the primary and try // again. c.logger.Error("primary sent invalid header -> replacing", "err", err) replaceErr := c.replacePrimaryProvider() if replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) // return original error return err } replacementHeader, replacementVals, fErr := c.signedHeaderAndValSetFromPrimary(newHeader.Height) if fErr != nil { c.logger.Error("Can't fetch header/vals from primary", "err", fErr) // return original error return err } if !bytes.Equal(replacementHeader.Hash(), newHeader.Hash()) || !bytes.Equal(replacementVals.Hash(), newVals.Hash()) { c.logger.Error("Replacement provider has a different header/vals", "newHash", newHeader.Hash(), "newVals", newVals.Hash(), "replHash", replacementHeader.Hash(), "replVals", replacementVals.Hash()) // return original error return err } // attempt to verify the header again return c.bisectionAgainstPrimary( initiallyTrustedHeader, initiallyTrustedVals, replacementHeader, replacementVals, now, ) case nil: // Compare header with the witnesses to ensure it's not a fork. // More witnesses we have, more chance to notice one. // // CORRECTNESS ASSUMPTION: there's at least 1 correct full node // (primary or one of the witnesses). if cmpErr := c.compareNewHeaderWithWitnesses(newHeader, now); cmpErr != nil { return cmpErr } default: return err } return 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 } // 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 { prevHeight := c.latestTrustedHeader.Height for { h, err := c.trustedStore.SignedHeaderBefore(prevHeight) if err == store.ErrSignedHeaderNotFound || (h != nil && h.Height <= height) { break } else if err != nil { return fmt.Errorf("failed to get header before %d: %w", prevHeight, err) } 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) } prevHeight = h.Height } c.latestTrustedHeader = nil c.latestTrustedVals = nil err := c.restoreTrustedHeaderAndVals() if err != nil { return err } return nil } func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { return fmt.Errorf("expected validator's hash %X, but got %X", h.ValidatorsHash, vals.Hash()) } if err := c.trustedStore.SaveSignedHeaderAndValidatorSet(h, vals); err != nil { return fmt.Errorf("failed to save trusted header: %w", err) } if c.pruningSize > 0 { if err := c.trustedStore.Prune(c.pruningSize); err != nil { return fmt.Errorf("prune: %w", err) } } if c.latestTrustedHeader == nil || h.Height > c.latestTrustedHeader.Height { c.latestTrustedHeader = h c.latestTrustedVals = vals } return nil } // 0 - latest header // Note it swaps the primary with a witness if primary is not responding after // MaxRetryAttempts. func (c *Client) signedHeaderAndValSetFromPrimary(height int64) (*types.SignedHeader, *types.ValidatorSet, error) { h, err := c.signedHeaderFromPrimary(height) if err != nil { return nil, nil, fmt.Errorf("can't fetch header: %w", err) } vals, err := c.validatorSetFromPrimary(height) if err != nil { return nil, nil, fmt.Errorf("can't fetch vals: %w", err) } return h, vals, nil } // 0 - latest header // Note it does not do retries nor swapping. func (c *Client) signedHeaderAndValSetFromWitness(height int64, witness provider.Provider) (*types.SignedHeader, *types.ValidatorSet, *errBadWitness) { h, err := witness.SignedHeader(height) if err != nil { return nil, nil, &errBadWitness{err, noResponse, -1} } err = c.validateHeader(h, height) if err != nil { return nil, nil, &errBadWitness{err, invalidHeader, -1} } vals, err := witness.ValidatorSet(height) if err != nil { return nil, nil, &errBadWitness{err, noResponse, -1} } err = c.validateValidatorSet(vals) if err != nil { return nil, nil, &errBadWitness{err, invalidValidatorSet, -1} } return h, vals, nil } func (c *Client) signedHeaderAndValSetFrom(height int64, source provider.Provider) (*types.SignedHeader, *types.ValidatorSet, error) { c.providerMutex.Lock() sourceIsPrimary := (c.primary == source) c.providerMutex.Unlock() if sourceIsPrimary { return c.signedHeaderAndValSetFromPrimary(height) } return c.signedHeaderAndValSetFromWitness(height, source) } // backwards verification (see VerifyHeaderBackwards func in the spec) verifies // headers before a trusted header. If a sent header is invalid the primary is // replaced with another provider and the operation is repeated. func (c *Client) backwards( initiallyTrustedHeader *types.SignedHeader, newHeader *types.SignedHeader, now time.Time) error { if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { c.logger.Error("Header Expired") 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 fmt.Errorf("failed to obtain the header at height #%d: %w", trustedHeader.Height-1, err) } c.logger.Debug("Verify newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", interimHeader.Height, "newHash", hash2str(interimHeader.Hash())) if err := VerifyBackwards(c.chainID, interimHeader, trustedHeader); err != nil { c.logger.Error("primary sent invalid header -> replacing", "err", err) if replaceErr := c.replacePrimaryProvider(); replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) // return original error return fmt.Errorf("verify backwards from %d to %d failed: %w", trustedHeader.Height, interimHeader.Height, err) } } 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, now time.Time) error { c.providerMutex.Lock() defer c.providerMutex.Unlock() // 1. Make sure AT LEAST ONE witness returns the same header. var ( headerMatched bool lastErrConfHeaders error ) for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { if len(c.witnesses) == 0 { return errNoWitnesses{} } // launch one goroutine per witness errc := make(chan error, len(c.witnesses)) for i, witness := range c.witnesses { go c.compareNewHeaderWithWitness(errc, h, witness, i, now) } witnessesToRemove := make([]int, 0) // handle errors as they come for i := 0; i < cap(errc); i++ { err := <-errc switch e := err.(type) { case nil: // at least one header matched headerMatched = true case ErrConflictingHeaders: // fork detected c.logger.Info("FORK DETECTED", "witness", e.Witness, "err", err) c.sendConflictingHeadersEvidence(&types.ConflictingHeadersEvidence{H1: h, H2: e.H2}) lastErrConfHeaders = e case errBadWitness: c.logger.Info("Bad witness", "witness", c.witnesses[e.WitnessIndex], "err", err) // if witness sent us invalid header / vals, remove it if e.Code == invalidHeader || e.Code == invalidValidatorSet { c.logger.Info("Witness sent us invalid header / vals -> removing it", "witness", c.witnesses[e.WitnessIndex]) witnessesToRemove = append(witnessesToRemove, e.WitnessIndex) } } } for _, idx := range witnessesToRemove { c.removeWitness(idx) } if lastErrConfHeaders != nil { // NOTE: all of the potential forks will be reported, but we only return // the last ErrConflictingHeaders error here. return lastErrConfHeaders } else if headerMatched { return nil } // 2. Otherwise, sleep time.Sleep(backoffTimeout(attempt)) } return errors.New("awaiting response from all witnesses exceeded dropout time") } func (c *Client) compareNewHeaderWithWitness(errc chan error, h *types.SignedHeader, witness provider.Provider, witnessIndex int, now time.Time) { altH, altVals, err := c.signedHeaderAndValSetFromWitness(h.Height, witness) if err != nil { err.WitnessIndex = witnessIndex errc <- err return } if !bytes.Equal(h.Hash(), altH.Hash()) { if bsErr := c.bisection(witness, c.latestTrustedHeader, c.latestTrustedVals, altH, altVals, now); bsErr != nil { errc <- errBadWitness{bsErr, invalidHeader, witnessIndex} return } errc <- ErrConflictingHeaders{H1: h, Primary: c.primary, H2: altH, Witness: witness} } errc <- nil } // 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] } } // Update attempts to advance the state by downloading the latest header and // comparing it with the existing one. It returns a new header on a successful // update. Otherwise, it returns nil (plus an error, if any). func (c *Client) Update(now time.Time) (*types.SignedHeader, error) { lastTrustedHeight, err := c.LastTrustedHeight() if err != nil { return nil, fmt.Errorf("can't get last trusted height: %w", err) } if lastTrustedHeight == -1 { // no headers yet => wait return nil, nil } latestHeader, latestVals, err := c.signedHeaderAndValSetFromPrimary(0) if err != nil { return nil, err } if latestHeader.Height > lastTrustedHeight { err = c.VerifyHeader(latestHeader, latestVals, now) if err != nil { return nil, err } c.logger.Info("Advanced to new state", "height", latestHeader.Height, "hash", hash2str(latestHeader.Hash())) return latestHeader, nil } return nil, 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() c.logger.Info("Primary is unavailable. Replacing with the first witness") if len(c.witnesses) <= 1 { return errNoWitnesses{} } 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, providerErr := c.primary.SignedHeader(height) c.providerMutex.Unlock() if providerErr == nil { err := c.validateHeader(h, height) if err != nil { replaceErr := c.replacePrimaryProvider() if replaceErr != nil { return nil, fmt.Errorf("%v. Tried to replace primary but: %w", err.Error(), replaceErr) } // replace primary and request signed header again return c.signedHeaderFromPrimary(height) } // valid header has been received return h, nil } if providerErr == provider.ErrSignedHeaderNotFound { return nil, providerErr } c.logger.Error("Failed to get signed header from primary", "attempt", attempt, "err", providerErr) time.Sleep(backoffTimeout(attempt)) } err := c.replacePrimaryProvider() if err != nil { return nil, fmt.Errorf("primary dropped out. Tried to replace but: %w", err) } return c.signedHeaderFromPrimary(height) } func (c *Client) validateHeader(h *types.SignedHeader, expectedHeight int64) error { if h == nil { return errors.New("nil header") } err := h.ValidateBasic(c.chainID) if err != nil { return err } if expectedHeight > 0 && h.Height != expectedHeight { return errors.New("height mismatch") } return nil } // 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, providerErr := c.primary.ValidatorSet(height) c.providerMutex.Unlock() if providerErr == nil { err := c.validateValidatorSet(vals) if err != nil { replaceErr := c.replacePrimaryProvider() if replaceErr != nil { return nil, fmt.Errorf("%v. Tried to replace primary but: %w", err.Error(), replaceErr) } // replace primary and request signed header again return c.validatorSetFromPrimary(height) } return vals, nil } if providerErr == provider.ErrValidatorSetNotFound { return vals, providerErr } c.logger.Error("Failed to get validator set from primary", "attempt", attempt, "err", providerErr) time.Sleep(backoffTimeout(attempt)) } err := c.replacePrimaryProvider() if err != nil { return nil, fmt.Errorf("primary dropped out. Tried to replace but: %w", err) } return c.validatorSetFromPrimary(height) } func (c *Client) validateValidatorSet(vals *types.ValidatorSet) error { if vals == nil { return errors.New("validator set is nil") } return vals.ValidateBasic() } // sendConflictingHeadersEvidence sends evidence to all witnesses and primary // on best effort basis. // // Evidence needs to be submitted to all full nodes since there's no way to // determine which full node is correct (honest). func (c *Client) sendConflictingHeadersEvidence(ev *types.ConflictingHeadersEvidence) { err := c.primary.ReportEvidence(ev) if err != nil { c.logger.Error("Failed to report evidence to primary", "ev", ev, "primary", c.primary) } for _, w := range c.witnesses { err := w.ReportEvidence(ev) if err != nil { c.logger.Error("Failed to report evidence to witness", "ev", ev, "witness", w) } } } // exponential backoff (with jitter) // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation func backoffTimeout(attempt uint16) time.Duration { // nolint:gosec // G404: Use of weak random number generator 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) }