Skip to content

Commit

Permalink
lite2: validate TrustOptions, add NewClientFromTrustedStore (#4374)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
melekes authored Feb 7, 2020
1 parent 66a544a commit b2832c6
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 16 deletions.
84 changes: 68 additions & 16 deletions lite2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 (
Expand All @@ -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.
Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
41 changes: 41 additions & 0 deletions lite2/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}

0 comments on commit b2832c6

Please sign in to comment.