diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d0446d314..5e3dc64b0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -21,6 +21,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [p2p] [\#4548](https://github.com/tendermint/tendermint/pull/4548) Add ban list to address book (@cmwaters) - [privval] \#4534 Add `error` as a return value on`GetPubKey()` - [Docker] \#4569 Default configuration added to docker image (you can still mount your own config the same way) (@greg-szabo) +- [lite2] [\#4562](https://github.com/tendermint/tendermint/pull/4562) Cache headers when using bisection (@cmwaters) ### BUG FIXES: diff --git a/docs/imgs/light_client_bisection_alg.png b/docs/imgs/light_client_bisection_alg.png new file mode 100644 index 000000000..2a12c7542 Binary files /dev/null and b/docs/imgs/light_client_bisection_alg.png differ diff --git a/lite2/client.go b/lite2/client.go index 22c080949..14c07e511 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -24,6 +24,11 @@ const ( 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 ) // Option sets a parameter for the light client. @@ -449,29 +454,6 @@ func (c *Client) compareWithLatestHeight(height int64) (int64, error) { 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). @@ -504,16 +486,17 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // VerifyHeader verifies new header against the trusted state. It returns // immediately if newHeader exists in trustedStore (no verification is -// needed). +// 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. +// 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 // // It returns ErrOldHeaderExpired if the latest trusted header expired. @@ -584,65 +567,6 @@ func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.Vali 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, @@ -707,6 +631,10 @@ func (c *Client) sequence( } // 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 by the primary 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( initiallyTrustedHeader *types.SignedHeader, initiallyTrustedVals *types.ValidatorSet, @@ -714,40 +642,53 @@ func (c *Client) bisection( 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 - - 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())) + "newHeight", headerCache[depth].sh.Height, + "newHash", hash2str(headerCache[depth].sh.Hash())) - err := Verify(c.chainID, trustedHeader, trustedVals, interimHeader, interimVals, c.trustingPeriod, now, - c.trustLevel) + err := Verify(c.chainID, trustedHeader, trustedVals, headerCache[depth].sh, headerCache[depth].valSet, + c.trustingPeriod, now, c.trustLevel) switch err.(type) { case nil: - if interimHeader.Height == newHeader.Height { + // Have we verified the last header + if depth == 0 { 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 + // 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: - pivotHeight := (interimHeader.Height + trustedHeader.Height) / 2 - interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(pivotHeight) - if err != nil { - return err + // do add another header to the end of the cache + if depth == len(headerCache)-1 { + pivotHeight := (headerCache[depth].sh.Height + trustedHeader. + Height) * bisectionNumerator / bisectionDenominator + interimHeader, interimVals, err := c.fetchHeaderAndValsAtHeight(pivotHeight) + if err != nil { + return err + } + headerCache = append(headerCache, headerSet{interimHeader, interimVals}) } + depth++ case ErrInvalidHeader: c.logger.Error("primary sent invalid header -> replacing", "err", err) @@ -756,16 +697,98 @@ func (c *Client) bisection( c.logger.Error("Can't replace primary", "err", replaceErr) // return original error return errors.Wrapf(err, "verify from #%d to #%d failed", - trustedHeader.Height, interimHeader.Height) + trustedHeader.Height, headerCache[depth].sh.Height) } // attempt to verify the header again continue default: return errors.Wrapf(err, "verify from #%d to #%d failed", - trustedHeader.Height, interimHeader.Height) + trustedHeader.Height, headerCache[depth].sh.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 +} + +// 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 } func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { diff --git a/lite2/doc.go b/lite2/doc.go index b61f5453f..f42aa64f1 100644 --- a/lite2/doc.go +++ b/lite2/doc.go @@ -97,6 +97,18 @@ Verify function verifies a new header against some trusted header. See https://github.com/tendermint/spec/blob/master/spec/consensus/light-client/verification.md for details. +There are two methods of verification: sequential and bisection + +Sequential uses the headers hashes and the validator sets to verify each adjacent header until +it reaches the target header. + +Bisection finds the middle header between a trusted and new header, reiterating the action until it +verifies a header. A cache of headers requested by the primary 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. + +refer to docs/imgs/light_client_bisection_alg.png + ## 3. Secure RPC proxy Tendermint RPC exposes a lot of info, but a malicious node could return any @@ -108,5 +120,8 @@ some other node. See https://docs.tendermint.com/master/tendermint-core/light-client-protocol.html for usage example. +Or see +https://github.com/tendermint/spec/tree/master/spec/consensus/light-client +for the full spec */ package lite