diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index b86714419..52d253d77 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -86,11 +86,11 @@ func runProxy(cmd *cobra.Command, args []string) error { }, httpp.NewWithClient(chainID, node), dbs.New(db, chainID), + lite.Logger(liteLogger), ) if err != nil { return err } - c.SetLogger(liteLogger) p := lproxy.Proxy{ Addr: listenAddr, diff --git a/lite2/auto_client.go b/lite2/auto_client.go deleted file mode 100644 index a5b2489c0..000000000 --- a/lite2/auto_client.go +++ /dev/null @@ -1,76 +0,0 @@ -package lite - -import ( - "time" - - "github.com/tendermint/tendermint/types" -) - -// AutoClient can auto update itself by fetching headers every N seconds. -type AutoClient struct { - base *Client - updatePeriod time.Duration - quit chan struct{} - - trustedHeaders chan *types.SignedHeader - errs chan error -} - -// NewAutoClient creates a new client and starts a polling goroutine. -func NewAutoClient(base *Client, updatePeriod time.Duration) *AutoClient { - c := &AutoClient{ - base: base, - updatePeriod: updatePeriod, - quit: make(chan struct{}), - trustedHeaders: make(chan *types.SignedHeader), - errs: make(chan error), - } - go c.autoUpdate() - return c -} - -// TrustedHeaders returns a channel onto which new trusted headers are posted. -func (c *AutoClient) TrustedHeaders() <-chan *types.SignedHeader { - return c.trustedHeaders -} - -// Err returns a channel onto which errors are posted. -func (c *AutoClient) Errs() <-chan error { - return c.errs -} - -// Stop stops the client. -func (c *AutoClient) Stop() { - close(c.quit) -} - -func (c *AutoClient) autoUpdate() { - ticker := time.NewTicker(c.updatePeriod) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - lastTrustedHeight, err := c.base.LastTrustedHeight() - if err != nil { - c.errs <- err - continue - } - - if lastTrustedHeight == -1 { - // no headers yet => wait - continue - } - - h, err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now()) - if err != nil { - // no header yet or verification error => try again after updatePeriod - c.errs <- err - continue - } - c.trustedHeaders <- h - case <-c.quit: - return - } - } -} diff --git a/lite2/auto_client_test.go b/lite2/auto_client_test.go deleted file mode 100644 index d80c60a9a..000000000 --- a/lite2/auto_client_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package lite - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - dbm "github.com/tendermint/tm-db" - - "github.com/tendermint/tendermint/libs/log" - mockp "github.com/tendermint/tendermint/lite2/provider/mock" - dbs "github.com/tendermint/tendermint/lite2/store/db" - "github.com/tendermint/tendermint/types" -) - -func TestAutoClient(t *testing.T) { - const ( - chainID = "TestAutoClient" - ) - - var ( - keys = genPrivKeys(4) - // 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! - vals = keys.ToValidators(20, 10) - bTime = time.Now().Add(-1 * time.Hour) - header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) - ) - - base, err := NewClient( - chainID, - TrustOptions{ - Period: 4 * time.Hour, - Height: 1, - Hash: header.Hash(), - }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - // interim header (3/3 signed) - 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - // last header (3/3 signed) - 3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - 4: vals, - }, - ), - dbs.New(dbm.NewMemDB(), chainID), - ) - require.NoError(t, err) - base.SetLogger(log.TestingLogger()) - - c := NewAutoClient(base, 1*time.Second) - defer c.Stop() - - for i := 2; i <= 3; i++ { - select { - case h := <-c.TrustedHeaders(): - assert.EqualValues(t, i, h.Height) - case err := <-c.Errs(): - require.NoError(t, err) - case <-time.After(2 * time.Second): - t.Fatal("no headers/errors received in 2 sec") - } - } -} diff --git a/lite2/client.go b/lite2/client.go index b705a932b..9d95817f0 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -48,6 +48,7 @@ const ( sequential mode = iota + 1 skipping + defaultUpdatePeriod = 5 * time.Second defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour ) @@ -90,6 +91,13 @@ func AlternativeSources(providers []provider.Provider) Option { } } +// 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 @@ -109,10 +117,20 @@ func ConfirmationFunction(fn func(action string) bool) Option { } } +// Logger option can be used to set a logger for the client. +func Logger(l log.Logger) Option { + return func(c *Client) { + c.logger = l + } +} + // Client represents a light client, connected to a single chain, which gets // headers from a primary provider, verifies them either sequentially or by // skipping some and stores them in a trusted store (usually, a local FS). // +// By default, the client will poll the primary provider for new headers every +// 5s (UpdatePeriod). If there are any, it will try to advance the state. +// // Default verification: SkippingVerification(DefaultTrustLevel) type Client struct { chainID string @@ -134,6 +152,7 @@ type Client struct { // Highest next validator set from the store (height=H+1). trustedNextVals *types.ValidatorSet + updatePeriod time.Duration removeNoLongerTrustedHeadersPeriod time.Duration confirmationFn func(action string) bool @@ -162,6 +181,7 @@ func NewClient( trustLevel: DefaultTrustLevel, primary: primary, trustedStore: trustedStore, + updatePeriod: defaultUpdatePeriod, removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod, confirmationFn: func(action string) bool { return true }, quit: make(chan struct{}), @@ -191,6 +211,10 @@ func NewClient( go c.removeNoLongerTrustedHeadersRoutine() } + if c.updatePeriod > 0 { + go c.autoUpdate() + } + return c, nil } @@ -339,16 +363,12 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { return c.updateTrustedHeaderAndVals(h, nextVals) } -// Stop stops the light client. +// Stop stops all the goroutines. If you wish to remove all the data, call +// Cleanup. func (c *Client) Stop() { close(c.quit) } -// SetLogger sets a logger. -func (c *Client) SetLogger(l log.Logger) { - c.logger = l -} - // TrustedHeader returns a trusted header at the given height (0 - the latest) // or nil if no such header exist. // @@ -361,7 +381,10 @@ func (c *Client) SetLogger(l log.Logger) { // - 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. +// - negative height is passed; +// - header is not found. +// +// 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") @@ -379,9 +402,6 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader if err != nil { return nil, err } - if h == nil { - return nil, nil - } // Ensure header can still be trusted. if HeaderExpired(h, c.trustingPeriod, now) { @@ -393,11 +413,23 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader // 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 } @@ -406,6 +438,8 @@ func (c *Client) ChainID() string { // 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) @@ -435,6 +469,10 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // 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", newVals.Hash()) @@ -708,10 +746,6 @@ func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { c.logger.Error("can't get a trusted header", "err", err, "height", height) continue } - if h == nil { - c.logger.Debug("attempted to remove non-existing header", "height", height) - continue - } // Stop if the header is within the trusting period. if !HeaderExpired(h, c.trustingPeriod, now) { @@ -725,3 +759,55 @@ func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { } } } + +func (c *Client) autoUpdate() { + ticker := time.NewTicker(c.updatePeriod) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + err := c.AutoUpdate(time.Now()) + if err != nil { + c.logger.Error("Error during auto update", "err", err) + } + case <-c.quit: + return + } + } +} + +// AutoUpdate 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) AutoUpdate(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 + } + + var i int64 + for err == nil { + // exponential increment: 1, 2, 4, 8, 16, ... + height := lastTrustedHeight + int64(1< 0. // - // If the store is empty and the latest SignedHeader is requested, an error - // is returned. + // If SignedHeader is not found, an error is returned. SignedHeader(height int64) (*types.SignedHeader, error) // ValidatorSet returns the ValidatorSet that corresponds to height. // // height must be > 0. // - // If the store is empty and the latest ValidatorSet is requested, an error - // is returned. + // If ValidatorSet is not found, an error is returned. ValidatorSet(height int64) (*types.ValidatorSet, error) // LastSignedHeaderHeight returns the last (newest) SignedHeader height.