mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-07 13:55:17 +00:00
lite2: move AutoClient into Client (#4326)
* lite2: move AutoClient into Client Most of the users will want auto update feature, so it makes sense to move it into the Client itself, rather than having a separate abstraction (it makes the code cleaner, but introduces an extra thing the user will need to learn). Also, add `FirstTrustedHeight` func to Client to get first trusted height. * fix db store tests * separate examples for auto and manual clients * AutoUpdate tries to update to latest state NOT 1 header at a time * fix errors * lite2: make Logger an option remove SetLogger func * fix lite cmd * lite2: make concurrency assumptions explicit * fixes after my own review * no need for nextHeightFn sequence func will download intermediate headers * correct comment
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
114
lite2/client.go
114
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<<uint(i))
|
||||
h, err := c.VerifyHeaderAtHeight(height, now)
|
||||
if err != nil {
|
||||
if errors.Is(err, provider.ErrSignedHeaderNotFound) {
|
||||
c.logger.Debug("No header yet", "at", height)
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, "failed to verify the header #%d", height)
|
||||
}
|
||||
c.logger.Info("Advanced to new state", "height", h.Height, "hash", h.Hash())
|
||||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -286,10 +287,10 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) {
|
||||
},
|
||||
),
|
||||
dbs.New(dbm.NewMemDB(), chainID),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer c.Stop()
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Verify new headers.
|
||||
_, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
@@ -303,7 +304,7 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) {
|
||||
|
||||
// Check expired headers are no longer available.
|
||||
h, err := c.TrustedHeader(1, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
// Check not expired headers are available.
|
||||
@@ -345,15 +346,15 @@ func TestClient_Cleanup(t *testing.T) {
|
||||
},
|
||||
),
|
||||
dbs.New(dbm.NewMemDB(), chainID),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
c.Cleanup()
|
||||
|
||||
// Check no headers exist after Cleanup.
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
|
||||
@@ -397,9 +398,9 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
@@ -436,9 +437,9 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
@@ -491,9 +492,9 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we still have the 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
@@ -536,13 +537,13 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we no longer have the invalid 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
}
|
||||
@@ -593,9 +594,9 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we still have the 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
@@ -605,7 +606,7 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) {
|
||||
|
||||
// Check we no longer have 2nd header (+header2+).
|
||||
h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
|
||||
@@ -643,9 +644,9 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) {
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we have swapped invalid 1st header (+header+) with correct one (+header1+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
@@ -655,7 +656,139 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) {
|
||||
|
||||
// Check we no longer have invalid 2nd header (+header2+).
|
||||
h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AutoUpdate(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClient_AutoUpdate"
|
||||
)
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
c, 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),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer c.Stop()
|
||||
|
||||
// should result in downloading & verifying headers #2 and #3
|
||||
err = c.AutoUpdate(bTime.Add(2 * time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err := c.TrustedHeader(2, bTime.Add(2*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, h)
|
||||
assert.EqualValues(t, 2, h.Height)
|
||||
|
||||
h, err = c.TrustedHeader(3, bTime.Add(2*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, h)
|
||||
assert.EqualValues(t, 3, h.Height)
|
||||
}
|
||||
|
||||
func TestClient_Concurrency(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClient_Concurrency"
|
||||
)
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
c, 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),
|
||||
UpdatePeriod(0),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// NOTE: Cleanup, Stop, VerifyHeaderAtHeight and Verify are not supposed
|
||||
// to be concurrenly safe.
|
||||
|
||||
assert.Equal(t, chainID, c.ChainID())
|
||||
|
||||
_, err := c.LastTrustedHeight()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = c.FirstTrustedHeight()
|
||||
assert.NoError(t, err)
|
||||
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/tendermint/tendermint/abci/example/kvstore"
|
||||
@@ -19,7 +17,8 @@ import (
|
||||
rpctest "github.com/tendermint/tendermint/rpc/test"
|
||||
)
|
||||
|
||||
func TestExample_Client(t *testing.T) {
|
||||
// Automatically getting new headers and verifying them.
|
||||
func TestExample_Client_AutoUpdate(t *testing.T) {
|
||||
// give Tendermint time to generate some blocks
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
@@ -58,16 +57,15 @@ func TestExample_Client(t *testing.T) {
|
||||
},
|
||||
provider,
|
||||
dbs.New(db, chainID),
|
||||
UpdatePeriod(1*time.Second),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
c.SetLogger(log.TestingLogger())
|
||||
defer c.Stop()
|
||||
|
||||
_, err = c.VerifyHeaderAtHeight(3, time.Now())
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
h, err := c.TrustedHeader(3, time.Now())
|
||||
if err != nil {
|
||||
@@ -78,7 +76,8 @@ func TestExample_Client(t *testing.T) {
|
||||
// Output: got header 3
|
||||
}
|
||||
|
||||
func TestExample_AutoClient(t *testing.T) {
|
||||
// Manually getting headers and verifying them.
|
||||
func TestExample_Client_ManualUpdate(t *testing.T) {
|
||||
// give Tendermint time to generate some blocks
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
@@ -108,7 +107,7 @@ func TestExample_AutoClient(t *testing.T) {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
base, err := NewClient(
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 504 * time.Hour, // 21 days
|
||||
@@ -117,29 +116,26 @@ func TestExample_AutoClient(t *testing.T) {
|
||||
},
|
||||
provider,
|
||||
dbs.New(db, chainID),
|
||||
UpdatePeriod(0),
|
||||
Logger(log.TestingLogger()),
|
||||
)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
base.SetLogger(log.TestingLogger())
|
||||
|
||||
c := NewAutoClient(base, 1*time.Second)
|
||||
defer c.Stop()
|
||||
|
||||
select {
|
||||
case h := <-c.TrustedHeaders():
|
||||
fmt.Println("got header", h.Height)
|
||||
// Output: got header 3
|
||||
case err := <-c.Errs():
|
||||
switch errors.Cause(err).(type) {
|
||||
case ErrOldHeaderExpired:
|
||||
// reobtain trust height and hash
|
||||
stdlog.Fatal(err)
|
||||
default:
|
||||
// try with another full node
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
_, err = c.VerifyHeaderAtHeight(3, time.Now())
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
h, err := c.TrustedHeader(3, time.Now())
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("got header", h.Height)
|
||||
// Output: got header 3
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
12
lite2/provider/errors.go
Normal file
12
lite2/provider/errors.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package provider
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrSignedHeaderNotFound is returned when a provider can't find the
|
||||
// requested header.
|
||||
ErrSignedHeaderNotFound = errors.New("signed header not found")
|
||||
// ErrValidatorSetNotFound is returned when a provider can't find the
|
||||
// requested validator set.
|
||||
ErrValidatorSetNotFound = errors.New("validator set not found")
|
||||
)
|
||||
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tendermint/tendermint/lite2/provider"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
||||
@@ -54,6 +55,10 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
|
||||
commit, err := p.client.Commit(h)
|
||||
if err != nil {
|
||||
// TODO: standartise errors on the RPC side
|
||||
if strings.Contains(err.Error(), "height must be less than or equal") {
|
||||
return nil, provider.ErrSignedHeaderNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -76,6 +81,10 @@ func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
const maxPerPage = 100
|
||||
res, err := p.client.Validators(h, 0, maxPerPage)
|
||||
if err != nil {
|
||||
// TODO: standartise errors on the RPC side
|
||||
if strings.Contains(err.Error(), "height must be less than or equal") {
|
||||
return nil, provider.ErrValidatorSetNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/tendermint/tendermint/lite2/provider"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
@@ -32,12 +30,12 @@ func (p *mock) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
if _, ok := p.headers[height]; ok {
|
||||
return p.headers[height], nil
|
||||
}
|
||||
return nil, errors.Errorf("no header at height %d", height)
|
||||
return nil, provider.ErrSignedHeaderNotFound
|
||||
}
|
||||
|
||||
func (p *mock) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
if _, ok := p.vals[height]; ok {
|
||||
return p.vals[height], nil
|
||||
}
|
||||
return nil, errors.Errorf("no vals for height %d", height)
|
||||
return nil, provider.ErrValidatorSetNotFound
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Provider interface {
|
||||
// If the provider fails to fetch the SignedHeader due to the IO or other
|
||||
// issues, an error will be returned.
|
||||
// If there's no SignedHeader for the given height, ErrSignedHeaderNotFound
|
||||
// will be returned.
|
||||
// error is returned.
|
||||
SignedHeader(height int64) (*types.SignedHeader, error)
|
||||
|
||||
// ValidatorSet returns the ValidatorSet that corresponds to height.
|
||||
@@ -28,6 +28,6 @@ type Provider interface {
|
||||
// If the provider fails to fetch the ValidatorSet due to the IO or other
|
||||
// issues, an error will be returned.
|
||||
// If there's no ValidatorSet for the given height, ErrValidatorSetNotFound
|
||||
// will be returned.
|
||||
// error is returned.
|
||||
ValidatorSet(height int64) (*types.ValidatorSet, error)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -78,7 +79,7 @@ func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
panic(err)
|
||||
}
|
||||
if len(bz) == 0 {
|
||||
return nil, nil
|
||||
return nil, errors.New("signed header not found")
|
||||
}
|
||||
|
||||
var signedHeader *types.SignedHeader
|
||||
@@ -97,7 +98,7 @@ func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
panic(err)
|
||||
}
|
||||
if len(bz) == 0 {
|
||||
return nil, nil
|
||||
return nil, errors.New("validator set not found")
|
||||
}
|
||||
|
||||
var valSet *types.ValidatorSet
|
||||
|
||||
@@ -42,11 +42,11 @@ func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) {
|
||||
|
||||
// Empty store
|
||||
h, err := dbStore.SignedHeader(1)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
valSet, err := dbStore.ValidatorSet(2)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, valSet)
|
||||
|
||||
// 1 key
|
||||
@@ -67,10 +67,10 @@ func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err = dbStore.SignedHeader(1)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
valSet, err = dbStore.ValidatorSet(2)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, valSet)
|
||||
}
|
||||
|
||||
@@ -21,16 +21,14 @@ type Store interface {
|
||||
//
|
||||
// height must be > 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.
|
||||
|
||||
Reference in New Issue
Block a user