mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-19 22:42:24 +00:00
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
This commit is contained in:
+68
-16
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user