From b2832c66afa2324cb38e288d7789469c0b7d6249 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 7 Feb 2020 14:37:20 +0100 Subject: [PATCH] lite2: validate TrustOptions, add NewClientFromTrustedStore (#4374) * validate trust options * add NewClientFromTrustedStore func * make maxRetryAttempts an option Closes #4370 * hash size should be equal to tmhash.Size * make maxRetryAttempts uint * make maxRetryAttempts uint16 maxRetryAttempts possible - 68 years * we do not store trustingPeriod * added test to create client from trusted store * remove header and vals from primary to make sure we're restoring them from the DB --- lite2/client.go | 84 +++++++++++++++++++++++++++++++++++--------- lite2/client_test.go | 41 +++++++++++++++++++++ 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index c6e2077dd..509f50534 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -9,6 +9,7 @@ import ( "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" @@ -43,6 +44,23 @@ type TrustOptions struct { 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 ( @@ -51,7 +69,7 @@ const ( defaultUpdatePeriod = 5 * time.Second defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour - maxRetryAttempts = 10 + defaultMaxRetryAttempts = 10 ) // Option sets a parameter for the light client. @@ -115,6 +133,14 @@ func Logger(l log.Logger) Option { } } +// 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). @@ -128,6 +154,7 @@ type Client struct { 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 @@ -173,11 +200,47 @@ func NewClient( 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: trustOptions.Period, + trustingPeriod: trustingPeriod, verificationMode: skipping, trustLevel: DefaultTrustLevel, + maxRetryAttempts: defaultMaxRetryAttempts, primary: primary, witnesses: witnesses, trustedStore: trustedStore, @@ -213,17 +276,6 @@ func NewClient( if err := c.restoreTrustedHeaderAndNextVals(); 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, nil } @@ -1010,7 +1062,7 @@ func (c *Client) replacePrimaryProvider() error { // 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 := 1; attempt <= maxRetryAttempts; attempt++ { + for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { c.providerMutex.Lock() h, err := c.primary.SignedHeader(height) c.providerMutex.Unlock() @@ -1039,7 +1091,7 @@ func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, err // 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 := 1; attempt <= maxRetryAttempts; attempt++ { + for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { c.providerMutex.Lock() vals, err := c.primary.ValidatorSet(height) c.providerMutex.Unlock() @@ -1060,6 +1112,6 @@ func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, err // exponential backoff (with jitter) // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation -func backoffTimeout(attempt int) time.Duration { +func backoffTimeout(attempt uint16) time.Duration { return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond } diff --git a/lite2/client_test.go b/lite2/client_test.go index 702473715..c5cfcd957 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -914,6 +914,7 @@ func TestProvider_Replacement(t *testing.T) { dbs.New(dbm.NewMemDB(), chainID), UpdatePeriod(0), Logger(log.TestingLogger()), + MaxRetryAttempts(1), ) require.NoError(t, err) err = c.Start() @@ -986,3 +987,43 @@ func TestProvider_TrustedHeaderFetchesMissingHeader(t *testing.T) { assert.Error(t, err) assert.Nil(t, h) } + +func Test_NewClientFromTrustedStore(t *testing.T) { + const ( + chainID = "Test_NewClientFromTrustedStore" + ) + + 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.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + primary = mockp.New( + chainID, + map[int64]*types.SignedHeader{}, + map[int64]*types.ValidatorSet{}, + ) + ) + + // 1) Initiate DB and fill with a "trusted" header + db := dbs.New(dbm.NewMemDB(), chainID) + err := db.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + // 2) Initialize Lite Client from Trusted Store + c, err := NewClientFromTrustedStore( + chainID, + 1*time.Hour, + primary, + []provider.Provider{primary}, + db, + ) + require.NoError(t, err) + + // 3) Check header exists through the lite clients eyes + h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + assert.NoError(t, err) + assert.EqualValues(t, 1, h.Height) +}