|
package lite
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/tendermint/tendermint/crypto/tmhash"
|
|
"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
|
|
}
|
|
|
|
// ValidateBasic performs basic validation.
|
|
func (opts TrustOptions) ValidateBasic() error {
|
|
if opts.Period <= 0 {
|
|
return errors.New("negative or zero period")
|
|
}
|
|
if opts.Height <= 0 {
|
|
return errors.New("negative or zero height")
|
|
}
|
|
if len(opts.Hash) != tmhash.Size {
|
|
return errors.Errorf("expected hash size to be %d bytes, got %d bytes",
|
|
tmhash.Size,
|
|
len(opts.Hash),
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mode byte
|
|
|
|
const (
|
|
sequential mode = iota + 1
|
|
skipping
|
|
|
|
defaultUpdatePeriod = 5 * time.Second
|
|
defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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).
|
|
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) {
|
|
|
|
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.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, 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,
|
|
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
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
matchedHeader := false
|
|
|
|
for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ {
|
|
// 1. Loop through all witnesses.
|
|
for _, witness := range c.witnesses {
|
|
|
|
// 2. Fetch the header.
|
|
altH, err := witness.SignedHeader(h.Height)
|
|
if err != nil {
|
|
c.logger.Info("No Response from witness ", "witness", witness)
|
|
continue
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
matchedHeader = true
|
|
|
|
}
|
|
|
|
// 4. Check that one responding witness has returned a matching header
|
|
if matchedHeader {
|
|
return nil
|
|
}
|
|
|
|
time.Sleep(backoffTimeout(attempt))
|
|
|
|
}
|
|
|
|
return errors.New("awaiting response from all witnesses exceeded dropout time")
|
|
}
|
|
|
|
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.FirstTrustedHeight()
|
|
if err != nil {
|
|
c.logger.Error("can't get first trusted height", "err", err)
|
|
return
|
|
}
|
|
if oldestHeight == -1 { // no headers yet => wait
|
|
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
|
|
}
|
|
if latestHeight == -1 { // no headers yet => wait
|
|
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 := 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
|
|
}
|