From 86adc2c89f80fa21579877d3f3e198f794e2ab9a Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 13 Jan 2020 11:56:48 +0400 Subject: [PATCH] lite: follow up from #3989 (#4209) * rename adjusted to adjacent Refs https://github.com/tendermint/tendermint/pull/3989#discussion_r352140829 * rename ErrTooMuchChange to ErrNotEnoughVotingPowerSigned Refs https://github.com/tendermint/tendermint/pull/3989#discussion_r352142785 * verify commit is properly signed * remove no longer trusted headers * restore trustedHeader and trustedNextVals * check trustedHeader using options Refs https://github.com/tendermint/tendermint/pull/4209#issuecomment-562462165 * use correct var when checking if headers are adjacent in bisection func + replace TODO with a comment https://github.com/tendermint/tendermint/pull/3989#discussion_r352125455 * return header in VerifyHeaderAtHeight because that way we avoid DB call + add godoc comments + check if there are no headers yet in AutoClient https://github.com/tendermint/tendermint/pull/3989#pullrequestreview-315454506 * TestVerifyAdjacentHeaders: add 2 more test-cases + add TestVerifyReturnsErrorIfTrustLevelIsInvalid * lite: avoid overflow when parsing key in db store! * lite: rename AutoClient#Err to Errs * lite: add a test for AutoClient * lite: fix keyPattern and call itr.Next in db store * lite: add two tests for db store * lite: add TestClientRemovesNoLongerTrustedHeaders * lite: test Client#Cleanup * lite: test restoring trustedHeader https://github.com/tendermint/tendermint/pull/4209#issuecomment-562462165 * lite: comment out unused code in test_helpers * fix TestVerifyReturnsErrorIfTrustLevelIsInvalid after merge * change defaultRemoveNoLongerTrustedHeadersPeriod and add docs * write more doc * lite: uncomment testable examples * use stdlog.Fatal to stop AutoClient tests * make lll linter happy * separate errors for 2 cases - the validator set of a skipped header cannot be trusted, i.e. <1/3rd of h1 validator set has signed (new error, something like ErrNewValSetCantBeTrusted) - the validator set is trusted but < 2/3rds has signed (ErrNewHeaderCantBeTrusted) https://github.com/tendermint/tendermint/pull/4209#discussion_r360331253 * remove all headers (even the last one) that are outside of the trusting period. By doing this, we avoid checking the trustedHeader's hash in checkTrustedHeaderUsingOptions (case #1). https://github.com/tendermint/tendermint/pull/4209#discussion_r360332460 * explain restoreTrustedHeaderAndNextVals better https://github.com/tendermint/tendermint/pull/4209#discussion_r360602328 * add ConfirmationFunction option for optionally prompting for user input Y/n before removing headers Refs https://github.com/tendermint/tendermint/pull/4209#discussion_r360602945 * make cleaning optional https://github.com/tendermint/tendermint/pull/4209#discussion_r364838189 * return error when user refused to remove headers * check for double votes in VerifyCommitTrusting * leave only ErrNewValSetCantBeTrusted error to differenciate between h2Vals.VerifyCommit and h1NextVals.VerifyCommitTrusting * fix example tests * remove unnecessary if condition https://github.com/tendermint/tendermint/pull/4209#discussion_r365171847 It will be handled by the above switch. * verifyCommitBasic does not depend on vals Co-authored-by: Marko --- lite/dynamic_verifier.go | 6 +- lite2/auto_client.go | 30 +-- lite2/auto_client_test.go | 76 +++++++ lite2/client.go | 355 ++++++++++++++++++++++++++---- lite2/client_test.go | 422 +++++++++++++++++++++++++++++++++++- lite2/doc.go | 76 +++++++ lite2/errors.go | 12 + lite2/example_test.go | 246 +++++++++++++-------- lite2/provider/http/http.go | 5 + lite2/rpc/client.go | 49 ++--- lite2/store/db/db.go | 72 ++++-- lite2/store/db/db_test.go | 76 +++++++ lite2/store/store.go | 19 +- lite2/test_helpers.go | 57 +++-- lite2/verifier.go | 32 ++- lite2/verifier_test.go | 69 ++++-- types/validator_set.go | 46 ++-- 17 files changed, 1371 insertions(+), 277 deletions(-) create mode 100644 lite2/auto_client_test.go create mode 100644 lite2/store/db/db_test.go diff --git a/lite/dynamic_verifier.go b/lite/dynamic_verifier.go index 8b69d2d7c..d4efdcbeb 100644 --- a/lite/dynamic_verifier.go +++ b/lite/dynamic_verifier.go @@ -185,7 +185,7 @@ func (dv *DynamicVerifier) Verify(shdr types.SignedHeader) error { // verifyAndSave will verify if this is a valid source full commit given the // best match trusted full commit, and if good, persist to dv.trusted. -// Returns ErrTooMuchChange when >2/3 of trustedFC did not sign sourceFC. +// Returns ErrNotEnoughVotingPowerSigned when >2/3 of trustedFC did not sign sourceFC. // Panics if trustedFC.Height() >= sourceFC.Height(). func (dv *DynamicVerifier) verifyAndSave(trustedFC, sourceFC FullCommit) error { if trustedFC.Height() >= sourceFC.Height() { @@ -247,8 +247,8 @@ FOR_LOOP: return sourceFC, nil } - // Handle special case when err is ErrTooMuchChange. - if types.IsErrTooMuchChange(err) { + // Handle special case when err is ErrNotEnoughVotingPowerSigned. + if types.IsErrNotEnoughVotingPowerSigned(err) { // Divide and conquer. start, end := trustedFC.Height(), sourceFC.Height() if !(start < end) { diff --git a/lite2/auto_client.go b/lite2/auto_client.go index 43341b4e0..a5b2489c0 100644 --- a/lite2/auto_client.go +++ b/lite2/auto_client.go @@ -13,7 +13,7 @@ type AutoClient struct { quit chan struct{} trustedHeaders chan *types.SignedHeader - err chan error + errs chan error } // NewAutoClient creates a new client and starts a polling goroutine. @@ -23,7 +23,7 @@ func NewAutoClient(base *Client, updatePeriod time.Duration) *AutoClient { updatePeriod: updatePeriod, quit: make(chan struct{}), trustedHeaders: make(chan *types.SignedHeader), - err: make(chan error), + errs: make(chan error), } go c.autoUpdate() return c @@ -35,8 +35,8 @@ func (c *AutoClient) TrustedHeaders() <-chan *types.SignedHeader { } // Err returns a channel onto which errors are posted. -func (c *AutoClient) Err() <-chan error { - return c.err +func (c *AutoClient) Errs() <-chan error { + return c.errs } // Stop stops the client. @@ -45,30 +45,30 @@ func (c *AutoClient) Stop() { } func (c *AutoClient) autoUpdate() { - lastTrustedHeight, err := c.base.LastTrustedHeight() - if err != nil { - c.err <- err - return - } - ticker := time.NewTicker(c.updatePeriod) defer ticker.Stop() for { select { case <-ticker.C: - err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now()) + lastTrustedHeight, err := c.base.LastTrustedHeight() if err != nil { - c.err <- err + c.errs <- err + continue + } + + if lastTrustedHeight == -1 { + // no headers yet => wait continue } - h, err := c.base.TrustedHeader(lastTrustedHeight+1, time.Now()) + + h, err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now()) if err != nil { - c.err <- err + // no header yet or verification error => try again after updatePeriod + c.errs <- err continue } c.trustedHeaders <- h - lastTrustedHeight = h.Height case <-c.quit: return } diff --git a/lite2/auto_client_test.go b/lite2/auto_client_test.go new file mode 100644 index 000000000..d80c60a9a --- /dev/null +++ b/lite2/auto_client_test.go @@ -0,0 +1,76 @@ +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 00ce448e9..cef0ab12a 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -47,14 +47,16 @@ type mode byte const ( sequential mode = iota + 1 skipping + + defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour ) // Option sets a parameter for the light client. type Option func(*Client) // SequentialVerification option configures the light client to sequentially -// check the headers. Note this is much slower than SkippingVerification, -// albeit more secure. +// check the headers (every header, in ascending height order). Note this is +// much slower than SkippingVerification, albeit more secure. func SequentialVerification() Option { return func(c *Client) { c.verificationMode = sequential @@ -68,7 +70,7 @@ func SequentialVerification() Option { // // trustLevel - fraction of the old validator set (in terms of voting power), // which must sign the new header in order for us to trust it. NOTE this only -// applies to non-adjusted headers. For adjusted headers, sequential +// applies to non-adjacent headers. For adjacent headers, sequential // verification is used. func SkippingVerification(trustLevel tmmath.Fraction) Option { if err := ValidateTrustLevel(trustLevel); err != nil { @@ -88,6 +90,25 @@ func AlternativeSources(providers []provider.Provider) Option { } } +// 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 +// started. +func RemoveNoLongerTrustedHeadersPeriod(d time.Duration) Option { + return func(c *Client) { + c.removeNoLongerTrustedHeadersPeriod = d + } +} + +// ConfirmationFunction option can be used to prompt to confirm an action. For +// example, remove newer headers if the light client is being reset with an +// older header. No confirmation is required by default! +func ConfirmationFunction(fn func(action string) bool) Option { + return func(c *Client) { + c.confirmationFn = fn + } +} + // 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). @@ -113,6 +134,12 @@ type Client struct { // Highest next validator set from the store (height=H+1). trustedNextVals *types.ValidatorSet + removeNoLongerTrustedHeadersPeriod time.Duration + + confirmationFn func(action string) bool + + quit chan struct{} + logger log.Logger } @@ -129,26 +156,136 @@ func NewClient( options ...Option) (*Client, error) { c := &Client{ - chainID: chainID, - trustingPeriod: trustOptions.Period, - verificationMode: skipping, - trustLevel: DefaultTrustLevel, - primary: primary, - trustedStore: trustedStore, - logger: log.NewNopLogger(), + chainID: chainID, + trustingPeriod: trustOptions.Period, + verificationMode: skipping, + trustLevel: DefaultTrustLevel, + primary: primary, + trustedStore: trustedStore, + removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod, + confirmationFn: func(action string) bool { return true }, + quit: make(chan struct{}), + logger: log.NewNopLogger(), } for _, o := range options { o(c) } - if err := c.initializeWithTrustOptions(trustOptions); err != nil { + 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 + } + } + + if c.removeNoLongerTrustedHeadersPeriod > 0 { + go c.removeNoLongerTrustedHeadersRoutine() + } return c, nil } +// Load trustedHeader and trustedNextVals from trustedStore. +func (c *Client) restoreTrustedHeaderAndNextVals() error { + lastHeight, err := c.trustedStore.LastSignedHeaderHeight() + if err != nil { + return errors.Wrap(err, "can't get last trusted header height") + } + + if lastHeight > 0 { + trustedHeader, err := c.trustedStore.SignedHeader(lastHeight) + if err != nil { + return errors.Wrap(err, "can't get last trusted header") + } + + trustedNextVals, err := c.trustedStore.ValidatorSet(lastHeight + 1) + if err != nil { + return errors.Wrap(err, "can't get last trusted next validators") + } + + c.trustedHeader = trustedHeader + c.trustedNextVals = trustedNextVals + } + + return nil +} + +// if options.Height: +// +// 1) ahead of trustedHeader.Height => fetch header (same height as +// trustedHeader) from primary provider and check it's hash matches the +// trustedHeader's hash (if not, remove trustedHeader and all the headers +// before) +// +// 2) equals trustedHeader.Height => check options.Hash matches the +// trustedHeader's hash (if not, remove trustedHeader and all the headers +// before) +// +// 3) behind trustedHeader.Height => remove all the headers between +// options.Height and trustedHeader.Height, update trustedHeader, then +// check options.Hash matches the trustedHeader's hash (if not, remove +// trustedHeader and all the headers before) +// +// The intuition here is the user is always right. I.e. if she decides to reset +// the light client with an older header, there must be a reason for it. +func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { + var primaryHash []byte + switch { + case options.Height > c.trustedHeader.Height: + h, err := c.primary.SignedHeader(c.trustedHeader.Height) + if err != nil { + return err + } + primaryHash = h.Hash() + case options.Height == c.trustedHeader.Height: + primaryHash = options.Hash + case options.Height < c.trustedHeader.Height: + action := fmt.Sprintf( + "Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)", + options.Height, options.Hash, + c.trustedHeader.Height, c.trustedHeader.Hash()) + if c.confirmationFn(action) { + // remove all the headers ( options.Height, trustedHeader.Height ] + c.cleanup(options.Height + 1) + // set c.trustedHeader to one at options.Height + c.restoreTrustedHeaderAndNextVals() + } else { + return errors.New("rollback aborted") + } + + primaryHash = options.Hash + } + + if !bytes.Equal(primaryHash, c.trustedHeader.Hash()) { + c.logger.Info("Prev. trusted header's hash %X doesn't match hash %X from primary provider", + c.trustedHeader.Hash(), primaryHash) + + action := fmt.Sprintf( + "Prev. trusted header's hash %X doesn't match hash %X from primary provider. Remove all the stored headers?", + c.trustedHeader.Hash(), primaryHash) + if c.confirmationFn(action) { + err := c.Cleanup() + if err != nil { + return errors.Wrap(err, "failed to cleanup") + } + } else { + return errors.New("refused to remove the stored headers despite hashes mismatch") + } + } + + return nil +} + +// Fetch trustedHeader and trustedNextVals from primary provider. func (c *Client) initializeWithTrustOptions(options TrustOptions) error { // 1) Fetch and verify the header. h, err := c.primary.SignedHeader(options.Height) @@ -158,21 +295,44 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { // NOTE: Verify func will check if it's expired or not. if err := h.ValidateBasic(c.chainID); err != nil { - return errors.Wrap(err, "ValidateBasic failed") + return err } if !bytes.Equal(h.Hash(), options.Hash) { return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash()) } - // 2) Fetch and verify the next vals. - vals, err := c.primary.ValidatorSet(options.Height + 1) + // 2) Fetch and verify the vals. + vals, err := c.primary.ValidatorSet(options.Height) + if err != nil { + return err + } + if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { + return errors.Errorf("expected header's validators (%X) to match those that were supplied (%X)", + h.ValidatorsHash, + vals.Hash(), + ) + } + // Ensure that +2/3 of validators signed correctly. + err = vals.VerifyCommit(c.chainID, h.Commit.BlockID, h.Height, h.Commit) + if err != nil { + return errors.Wrap(err, "invalid commit") + } + + // 3) Fetch and verify the next vals (verification happens in + // updateTrustedHeaderAndVals). + nextVals, err := c.primary.ValidatorSet(options.Height + 1) if err != nil { return err } - // 3) Persist both of them and continue. - return c.updateTrustedHeaderAndVals(h, vals) + // 4) Persist both of them and continue. + return c.updateTrustedHeaderAndVals(h, nextVals) +} + +// Stop stops the light client. +func (c *Client) Stop() { + close(c.quit) } // SetLogger sets a logger. @@ -182,15 +342,17 @@ func (c *Client) SetLogger(l log.Logger) { // TrustedHeader returns a trusted header at the given height (0 - the latest) // or nil if no such header exist. -// TODO: mention how many headers will be kept by the light client. +// +// Headers, which can't be trusted anymore, are removed once a day (can be +// changed with RemoveNoLongerTrustedHeadersPeriod option). // . // height must be >= 0. // // It returns an error if: -// - the header expired (ErrOldHeaderExpired). In that case, update your -// client to more recent height; -// - there are some issues with the trusted store, although that should not -// happen normally. +// - 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. func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) { if height < 0 { return nil, errors.New("negative height") @@ -208,22 +370,25 @@ 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. - expirationTime := h.Time.Add(c.trustingPeriod) - if !expirationTime.After(now) { - return nil, ErrOldHeaderExpired{expirationTime, now} + if HeaderExpired(h, c.trustingPeriod, now) { + return nil, ErrOldHeaderExpired{h.Time.Add(c.trustingPeriod), now} } return h, nil } -// LastTrustedHeight returns a last trusted height. +// LastTrustedHeight returns a last trusted height. -1 and nil are returned if +// there are no trusted headers. func (c *Client) LastTrustedHeight() (int64, error) { return c.trustedStore.LastSignedHeaderHeight() } -// ChainID returns the chain ID. +// ChainID returns the chain ID the light client was configured with. func (c *Client) ChainID() string { return c.chainID } @@ -232,18 +397,18 @@ func (c *Client) ChainID() string { // and calls VerifyHeader. // // If the trusted header is more recent than one here, an error is returned. -func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) error { +func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { if c.trustedHeader.Height >= height { - return errors.Errorf("height #%d is already trusted (last: #%d)", height, c.trustedHeader.Height) + return nil, errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) } // Request the header and the vals. newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height) if err != nil { - return err + return nil, err } - return c.VerifyHeader(newHeader, newVals, now) + return newHeader, c.VerifyHeader(newHeader, newVals, now) } // VerifyHeader verifies new header against the trusted state. @@ -255,13 +420,13 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) error { // SkippingVerification(trustLevel): verifies that {trustLevel} of the trusted // validator set has signed the new header. If it's not the case and the // headers are not adjacent, bisection is performed and necessary (not all) -// intermediate headers will be requested. See the specification for the -// algorithm. +// intermediate headers will be requested. See the specification for details. +// 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. func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { if c.trustedHeader.Height >= newHeader.Height { - return errors.Errorf("height #%d is already trusted (last: #%d)", newHeader.Height, c.trustedHeader.Height) + return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) } if len(c.alternatives) > 0 { @@ -291,6 +456,44 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali return c.updateTrustedHeaderAndVals(newHeader, nextVals) } +// Cleanup removes all the data (headers and validator sets) stored. +func (c *Client) Cleanup() error { + return c.cleanup(0) +} + +// stopHeight=0 -> remove all data +func (c *Client) cleanup(stopHeight int64) error { + // 1) Get the oldest height. + oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight() + if err != nil { + return errors.Wrap(err, "can't get first trusted height") + } + + // 2) Get the latest height. + latestHeight, err := c.trustedStore.LastSignedHeaderHeight() + if err != nil { + return errors.Wrap(err, "can't get last trusted height") + } + + // 3) Remove all headers and validator sets. + if stopHeight == 0 { + stopHeight = oldestHeight + } + for height := stopHeight; height <= latestHeight; height++ { + err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height) + if err != nil { + c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) + continue + } + } + + c.trustedHeader = nil + c.trustedNextVals = nil + + return nil +} + +// see VerifyHeader func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { // 1) Verify any intermediate headers. var ( @@ -329,6 +532,7 @@ func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.Validato return Verify(c.chainID, c.trustedHeader, c.trustedNextVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel) } +// see VerifyHeader func (c *Client) bisection( lastHeader *types.SignedHeader, lastVals *types.ValidatorSet, @@ -340,15 +544,10 @@ func (c *Client) bisection( switch err.(type) { case nil: return nil - case types.ErrTooMuchChange: + case ErrNewValSetCantBeTrusted: // continue bisection default: - return errors.Wrapf(err, "failed to verify the header #%d ", newHeader.Height) - } - - if newHeader.Height == c.trustedHeader.Height+1 { - // TODO: submit evidence here - return errors.Errorf("adjacent headers (#%d and #%d) that are not matching", lastHeader.Height, newHeader.Height) + return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) } pivot := (c.trustedHeader.Height + newHeader.Header.Height) / 2 @@ -392,22 +591,23 @@ func (c *Client) bisection( return nil } -func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { - if !bytes.Equal(h.NextValidatorsHash, vals.Hash()) { - return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, vals.Hash()) +// persist header and next validators to trustedStore. +func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error { + if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) { + return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, nextVals.Hash()) } - if err := c.trustedStore.SaveSignedHeader(h); err != nil { + if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil { return errors.Wrap(err, "failed to save trusted header") } - if err := c.trustedStore.SaveValidatorSet(vals, h.Height+1); err != nil { - return errors.Wrap(err, "failed to save trusted vals") - } + c.trustedHeader = h - c.trustedNextVals = vals + c.trustedNextVals = nextVals + return nil } +// fetch header and validators for the given height from primary provider. func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) { h, err := c.primary.SignedHeader(height) if err != nil { @@ -420,6 +620,7 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, return h, vals, nil } +// compare header with one from a random alternative provider. func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) error { // 1. Pick an alternative provider. p := c.alternatives[tmrand.Intn(len(c.alternatives))] @@ -442,3 +643,61 @@ func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) er return nil } + +func (c *Client) removeNoLongerTrustedHeadersRoutine() { + ticker := time.NewTicker(c.removeNoLongerTrustedHeadersPeriod) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.RemoveNoLongerTrustedHeaders(time.Now()) + case <-c.quit: + return + } + } +} + +// RemoveNoLongerTrustedHeaders removes no longer trusted headers (due to +// expiration). +// +// Exposed for testing. +func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { + // 1) Get the oldest height. + oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight() + if err != nil { + c.logger.Error("can't get first trusted height", "err", err) + return + } + + // 2) Get the latest height. + latestHeight, err := c.LastTrustedHeight() + if err != nil { + c.logger.Error("can't get last trusted height", "err", err) + return + } + + // 3) Remove all headers that are outside of the trusting period. + for height := oldestHeight; height <= latestHeight; height++ { + h, err := c.trustedStore.SignedHeader(height) + if err != nil { + 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) { + break + } + + err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height) + if err != nil { + c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) + continue + } + } +} diff --git a/lite2/client_test.go b/lite2/client_test.go index 6052fac8c..c7bafd6bb 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -9,6 +9,7 @@ import ( 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" @@ -135,8 +136,9 @@ func TestClient_SequentialVerification(t *testing.T) { } else { require.NoError(t, err) } + defer c.Stop() - err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) if tc.verifyErr { assert.Error(t, err) } else { @@ -232,8 +234,9 @@ func TestClient_SkippingVerification(t *testing.T) { } else { require.NoError(t, err) } + defer c.Stop() - err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) if tc.verifyErr { assert.Error(t, err) } else { @@ -241,3 +244,418 @@ func TestClient_SkippingVerification(t *testing.T) { } } } + +func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { + const ( + chainID = "TestClientRemovesNoLongerTrustedHeaders" + ) + + 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(2*time.Hour), 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(4*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) + defer c.Stop() + c.SetLogger(log.TestingLogger()) + + // Verify new headers. + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second)) + require.NoError(t, err) + now := bTime.Add(4 * time.Hour).Add(1 * time.Second) + _, err = c.VerifyHeaderAtHeight(3, now) + require.NoError(t, err) + + // Remove expired headers. + c.RemoveNoLongerTrustedHeaders(now) + + // Check expired headers are no longer available. + h, err := c.TrustedHeader(1, now) + assert.NoError(t, err) + assert.Nil(t, h) + + // Check not expired headers are available. + h, err = c.TrustedHeader(2, now) + assert.NoError(t, err) + assert.NotNil(t, h) +} + +func TestClient_Cleanup(t *testing.T) { + const ( + chainID = "TestClient_Cleanup" + ) + + 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, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ), + dbs.New(dbm.NewMemDB(), chainID), + ) + 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.Nil(t, h) +} + +// trustedHeader.Height == options.Height +func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { + const ( + chainID = "TestClientRestoreTrustedHeaderAfterStartup1" + ) + + 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)) + ) + + // 1. options.Hash == trustedHeader.Hash + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + 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, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ), + trustedStore, + ) + require.NoError(t, err) + c.SetLogger(log.TestingLogger()) + + h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, h) + assert.Equal(t, h.Hash(), header.Hash()) + } + + // 2. options.Hash != trustedHeader.Hash + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + // header1 != header + header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), 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: header1.Hash(), + }, + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header1, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ), + trustedStore, + ) + require.NoError(t, err) + c.SetLogger(log.TestingLogger()) + + h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, h) + assert.Equal(t, h.Hash(), header1.Hash()) + } +} + +// trustedHeader.Height < options.Height +func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { + const ( + chainID = "TestClientRestoreTrustedHeaderAfterStartup2" + ) + + 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)) + ) + + // 1. options.Hash == trustedHeader.Hash + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), 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: 2, + Hash: header2.Hash(), + }, + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ), + trustedStore, + ) + 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)) + assert.NoError(t, err) + assert.NotNil(t, h) + assert.Equal(t, h.Hash(), header.Hash()) + } + + // 2. options.Hash != trustedHeader.Hash + // This could happen if previous provider was lying to us. + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + // header1 != header + header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + + header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), 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: 2, + Hash: header2.Hash(), + }, + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header1, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ), + trustedStore, + ) + 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.Nil(t, h) + } +} + +// trustedHeader.Height > options.Height +func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { + const ( + chainID = "TestClientRestoreTrustedHeaderAfterStartup3" + ) + + 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)) + ) + + // 1. options.Hash == trustedHeader.Hash + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals) + require.NoError(t, err) + + c, err := NewClient( + chainID, + TrustOptions{ + Period: 4 * time.Hour, + Height: 1, + Hash: header.Hash(), + }, + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ), + trustedStore, + ) + 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)) + assert.NoError(t, err) + assert.NotNil(t, h) + assert.Equal(t, h.Hash(), header.Hash()) + + // 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.Nil(t, h) + } + + // 2. options.Hash != trustedHeader.Hash + // This could happen if previous provider was lying to us. + { + trustedStore := dbs.New(dbm.NewMemDB(), chainID) + err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) + require.NoError(t, err) + + // header1 != header + header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + + header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals) + require.NoError(t, err) + + c, err := NewClient( + chainID, + TrustOptions{ + Period: 4 * time.Hour, + Height: 1, + Hash: header1.Hash(), + }, + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header1, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ), + trustedStore, + ) + 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)) + assert.NoError(t, err) + assert.NotNil(t, h) + assert.Equal(t, h.Hash(), header1.Hash()) + + // 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.Nil(t, h) + } +} diff --git a/lite2/doc.go b/lite2/doc.go index 7ed3ec50e..920032f36 100644 --- a/lite2/doc.go +++ b/lite2/doc.go @@ -31,5 +31,81 @@ Subjectivity](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak NOTE: Tendermint provides a somewhat different (stronger) light client model than Bitcoin under eclipse, since the eclipsing node(s) can only fool the light client if they have two-thirds of the private keys from the last root-of-trust. + +# Common structures + +* SignedHeader + +SignedHeader is a block header along with a commit -- enough validator +precommit-vote signatures to prove its validity (> 2/3 of the voting power) +given the validator set responsible for signing that header. + +The hash of the next validator set is included and signed in the SignedHeader. +This lets the lite client keep track of arbitrary changes to the validator set, +as every change to the validator set must be approved by inclusion in the +header and signed in the commit. + +In the worst case, with every block changing the validators around completely, +a lite client can sync up with every block header to verify each validator set +change on the chain. In practice, most applications will not have frequent +drastic updates to the validator set, so the logic defined in this package for +lite client syncing is optimized to use intelligent bisection. + +# What this package provides + +This package provides three major things: + +1. Client implementation (see client.go) +2. Pure functions to verify a new header (see verifier.go) +3. Secure RPC proxy + +## 1. Client implementation (see client.go) + +Example usage: + + db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) + if err != nil { + // return err + t.Fatal(err) + } + c, err := NewClient( + chainID, + TrustOptions{ + Period: 504 * time.Hour, // 21 days + Height: 100, + Hash: header.Hash(), + }, + httpp.New(chainID, "tcp://localhost:26657"), + dbs.New(db, chainID), + ) + + err = c.VerifyHeaderAtHeight(101, time.Now()) + if err != nil { + fmt.Println("retry?") + } + + h, err := c.TrustedHeader(101) + if err != nil { + fmt.Println("retry?") + } + fmt.Println("got header", h) + +## 2. Pure functions to verify a new header (see verifier.go) + +Verify function verifies a new header against some trusted header. See +https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md +for details. + +## 3. Secure RPC proxy + +Tendermint RPC exposes a lot of info, but a malicious node could return any +data it wants to queries, or even to block headers, even making up fake +signatures from non-existent validators to justify it. Secure RPC proxy serves +as a wrapper, which verifies all the headers, using a light client connected to +some other node. + +See +https://github.com/tendermint/tendermint/blob/master/cmd/tendermint/commands/lite.go +for usage example. */ package lite diff --git a/lite2/errors.go b/lite2/errors.go index 608f9b3ef..d0b5d2d31 100644 --- a/lite2/errors.go +++ b/lite2/errors.go @@ -3,6 +3,8 @@ package lite import ( "fmt" "time" + + "github.com/tendermint/tendermint/types" ) // ErrOldHeaderExpired means the old (trusted) header has expired according to @@ -16,3 +18,13 @@ type ErrOldHeaderExpired struct { func (e ErrOldHeaderExpired) Error() string { return fmt.Sprintf("old header has expired at %v (now: %v)", e.At, e.Now) } + +// ErrNewValSetCantBeTrusted means the new validator set cannot be trusted +// because < 1/3rd (+trustLevel+) of the old validator set has signed. +type ErrNewValSetCantBeTrusted struct { + Reason types.ErrNotEnoughVotingPowerSigned +} + +func (e ErrNewValSetCantBeTrusted) Error() string { + return fmt.Sprintf("cant trust new val set: %v", e.Reason) +} diff --git a/lite2/example_test.go b/lite2/example_test.go index 19a27d499..5afa07900 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -1,95 +1,155 @@ package lite -//func TestExample_Client(t *testing.T) { -// const ( -// chainID = "my-awesome-chain" -// ) -// dbDir, err := ioutil.TempDir("", "lite-client-example") -// if err != nil { -// t.Fatal(err) -// } -// defer os.RemoveAll(dbDir) - -// // TODO: fetch the "trusted" header from a node -// header := (*types.SignedHeader)(nil) - -// ///////////////////////////////////////////////////////////////////////////// - -// db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) -// if err != nil { -// // return err -// t.Fatal(err) -// } -// c, err := NewClient( -// chainID, -// TrustOptions{ -// Period: 504 * time.Hour, // 21 days -// Height: 100, -// Hash: header.Hash(), -// }, -// httpp.New(chainID, "tcp://localhost:26657"), -// dbs.New(db, chainID), -// ) - -// err = c.VerifyHeaderAtHeight(101, time.Now()) -// if err != nil { -// fmt.Println("retry?") -// } - -// h, err := c.TrustedHeader(101) -// if err != nil { -// fmt.Println("retry?") -// } -// fmt.Println("got header", h) -// // verify some data -//} - -//func TestExample_AutoClient(t *testing.T) { -// const ( -// chainID = "my-awesome-chain" -// ) -// dbDir, err := ioutil.TempDir("", "lite-client-example") -// if err != nil { -// t.Fatal(err) -// } -// defer os.RemoveAll(dbDir) - -// // TODO: fetch the "trusted" header from a node -// header := (*types.SignedHeader)(nil) - -// ///////////////////////////////////////////////////////////////////////////// - -// db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) -// if err != nil { -// // return err -// t.Fatal(err) -// } - -// base, err := NewClient( -// chainID, -// TrustOptions{ -// Period: 504 * time.Hour, // 21 days -// Height: 100, -// Hash: header.Hash(), -// }, -// httpp.New(chainID, "tcp://localhost:26657"), -// dbs.New(db, chainID), -// ) - -// c := NewAutoClient(base, 1*time.Second) -// defer c.Stop() - -// select { -// case h := <-c.TrustedHeaders(): -// fmt.Println("got header", h) -// // verify some data -// case err := <-c.Err(): -// switch errors.Cause(err).(type) { -// case ErrOldHeaderExpired: -// // reobtain trust height and hash -// default: -// // try with another full node -// fmt.Println("got error", err) -// } -// } -//} +import ( + "fmt" + "io/ioutil" + stdlog "log" + "os" + "testing" + "time" + + "github.com/pkg/errors" + + dbm "github.com/tendermint/tm-db" + + "github.com/tendermint/tendermint/abci/example/kvstore" + "github.com/tendermint/tendermint/libs/log" + httpp "github.com/tendermint/tendermint/lite2/provider/http" + dbs "github.com/tendermint/tendermint/lite2/store/db" + rpctest "github.com/tendermint/tendermint/rpc/test" +) + +func TestExample_Client(t *testing.T) { + // give Tendermint time to generate some blocks + time.Sleep(5 * time.Second) + + dbDir, err := ioutil.TempDir("", "lite-client-example") + if err != nil { + stdlog.Fatal(err) + } + defer os.RemoveAll(dbDir) + + var ( + config = rpctest.GetConfig() + chainID = config.ChainID() + ) + + provider, err := httpp.New(chainID, config.RPC.ListenAddress) + if err != nil { + stdlog.Fatal(err) + } + + header, err := provider.SignedHeader(2) + if err != nil { + stdlog.Fatal(err) + } + + db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) + if err != nil { + stdlog.Fatal(err) + } + + c, err := NewClient( + chainID, + TrustOptions{ + Period: 504 * time.Hour, // 21 days + Height: 2, + Hash: header.Hash(), + }, + provider, + dbs.New(db, chainID), + ) + if err != nil { + stdlog.Fatal(err) + } + c.SetLogger(log.TestingLogger()) + + _, 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 TestExample_AutoClient(t *testing.T) { + // give Tendermint time to generate some blocks + time.Sleep(5 * time.Second) + + dbDir, err := ioutil.TempDir("", "lite-client-example") + if err != nil { + stdlog.Fatal(err) + } + defer os.RemoveAll(dbDir) + + var ( + config = rpctest.GetConfig() + chainID = config.ChainID() + ) + + provider, err := httpp.New(chainID, config.RPC.ListenAddress) + if err != nil { + stdlog.Fatal(err) + } + + header, err := provider.SignedHeader(2) + if err != nil { + stdlog.Fatal(err) + } + + db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) + if err != nil { + stdlog.Fatal(err) + } + + base, err := NewClient( + chainID, + TrustOptions{ + Period: 504 * time.Hour, // 21 days + Height: 2, + Hash: header.Hash(), + }, + provider, + dbs.New(db, chainID), + ) + 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) + } + } +} + +func TestMain(m *testing.M) { + // start a tendermint node (and kvstore) in the background to test against + app := kvstore.NewApplication() + node := rpctest.StartTendermint(app) + + code := m.Run() + + // and shut down proper at the end + rpctest.StopTendermint(node) + os.Exit(code) +} diff --git a/lite2/provider/http/http.go b/lite2/provider/http/http.go index 53bebc8c2..24c9d5527 100644 --- a/lite2/provider/http/http.go +++ b/lite2/provider/http/http.go @@ -39,10 +39,13 @@ func NewWithClient(chainID string, client SignStatusClient) provider.Provider { } } +// ChainID returns a chainID this provider was configured with. func (p *http) ChainID() string { return p.chainID } +// SignedHeader fetches a SignedHeader at the given height and checks the +// chainID matches. func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) { h, err := validateHeight(height) if err != nil { @@ -62,6 +65,8 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) { return &commit.SignedHeader, nil } +// ValidatorSet fetches a ValidatorSet at the given height. Multiple HTTP +// requests might be required if the validator set size is over 100. func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) { h, err := validateHeight(height) if err != nil { diff --git a/lite2/rpc/client.go b/lite2/rpc/client.go index fe940ea74..458e871ad 100644 --- a/lite2/rpc/client.go +++ b/lite2/rpc/client.go @@ -90,14 +90,10 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes, } // Update the light client if we're behind. - if err := c.updateLiteClientIfNeededTo(resp.Height + 1); err != nil { - return nil, err - } - - // AppHash for height H is in header H+1. - h, err := c.lc.TrustedHeader(resp.Height+1, time.Now()) + // NOTE: AppHash for height H is in header H+1. + h, err := c.updateLiteClientIfNeededTo(resp.Height + 1) if err != nil { - return nil, errors.Wrapf(err, "TrustedHeader(%d)", resp.Height+1) + return nil, err } // Validate the value proof against the trusted header. @@ -188,7 +184,7 @@ func (c *Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlock // Update the light client if we're behind. if len(res.BlockMetas) > 0 { lastHeight := res.BlockMetas[len(res.BlockMetas)-1].Header.Height - if err := c.updateLiteClientIfNeededTo(lastHeight); err != nil { + if _, err := c.updateLiteClientIfNeededTo(lastHeight); err != nil { return nil, err } } @@ -232,15 +228,12 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) { } // Update the light client if we're behind. - if err := c.updateLiteClientIfNeededTo(res.Block.Height); err != nil { + h, err := c.updateLiteClientIfNeededTo(res.Block.Height) + if err != nil { return nil, err } // Verify block. - h, err := c.lc.TrustedHeader(res.Block.Height, time.Now()) - if err != nil { - return nil, errors.Wrapf(err, "TrustedHeader(%d)", res.Block.Height) - } if bH, tH := res.Block.Hash(), h.Hash(); !bytes.Equal(bH, tH) { return nil, errors.Errorf("Block#Header %X does not match with trusted header %X", bH, tH) @@ -265,15 +258,12 @@ func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) { } // Update the light client if we're behind. - if err := c.updateLiteClientIfNeededTo(res.Height); err != nil { + h, err := c.updateLiteClientIfNeededTo(res.Height) + if err != nil { return nil, err } // Verify commit. - h, err := c.lc.TrustedHeader(res.Height, time.Now()) - if err != nil { - return nil, errors.Wrapf(err, "TrustedHeader(%d)", res.Height) - } if rH, tH := res.Hash(), h.Hash(); !bytes.Equal(rH, tH) { return nil, errors.Errorf("header %X does not match with trusted header %X", rH, tH) @@ -296,15 +286,12 @@ func (c *Client) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { } // Update the light client if we're behind. - if err := c.updateLiteClientIfNeededTo(res.Height); err != nil { + h, err := c.updateLiteClientIfNeededTo(res.Height) + if err != nil { return nil, err } // Validate the proof. - h, err := c.lc.TrustedHeader(res.Height, time.Now()) - if err != nil { - return res, errors.Wrapf(err, "TrustedHeader(%d)", res.Height) - } return res, res.Proof.Validate(h.DataHash) } @@ -333,17 +320,21 @@ func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { return c.next.UnsubscribeAll(ctx, subscriber) } -func (c *Client) updateLiteClientIfNeededTo(height int64) error { +func (c *Client) updateLiteClientIfNeededTo(height int64) (*types.SignedHeader, error) { lastTrustedHeight, err := c.lc.LastTrustedHeight() if err != nil { - return errors.Wrap(err, "LastTrustedHeight") + return nil, errors.Wrap(err, "LastTrustedHeight") } + if lastTrustedHeight < height { - if err := c.lc.VerifyHeaderAtHeight(height, time.Now()); err != nil { - return errors.Wrapf(err, "VerifyHeaderAtHeight(%d)", height) - } + return c.lc.VerifyHeaderAtHeight(height, time.Now()) } - return nil + + h, err := c.lc.TrustedHeader(height, time.Now()) + if err != nil { + return nil, errors.Wrapf(err, "TrustedHeader(#%d)", height) + } + return h, nil } func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) { diff --git a/lite2/store/db/db.go b/lite2/store/db/db.go index c6961666e..4d72f9425 100644 --- a/lite2/store/db/db.go +++ b/lite2/store/db/db.go @@ -1,7 +1,6 @@ package db import ( - "errors" "fmt" "regexp" "strconv" @@ -23,39 +22,57 @@ type dbs struct { // New returns a Store that wraps any DB (with an optional prefix in case you // want to use one DB with many light clients). +// +// Objects are marshalled using amino (github.com/tendermint/go-amino) func New(db dbm.DB, prefix string) store.Store { cdc := amino.NewCodec() cryptoAmino.RegisterAmino(cdc) return &dbs{db: db, prefix: prefix, cdc: cdc} } -func (s *dbs) SaveSignedHeader(sh *types.SignedHeader) error { +// SaveSignedHeaderAndNextValidatorSet persists SignedHeader and ValidatorSet +// to the db. +func (s *dbs) SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error { if sh.Height <= 0 { panic("negative or zero height") } + // TODO: batch bz, err := s.cdc.MarshalBinaryLengthPrefixed(sh) if err != nil { return err } s.db.Set(s.shKey(sh.Height), bz) + + bz, err = s.cdc.MarshalBinaryLengthPrefixed(valSet) + if err != nil { + return err + } + s.db.Set(s.vsKey(sh.Height+1), bz) + return nil } -func (s *dbs) SaveValidatorSet(valSet *types.ValidatorSet, height int64) error { +// DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader and ValidatorSet +// from the db. +func (s *dbs) DeleteSignedHeaderAndNextValidatorSet(height int64) error { if height <= 0 { panic("negative or zero height") } - bz, err := s.cdc.MarshalBinaryLengthPrefixed(valSet) - if err != nil { - return err - } - s.db.Set(s.vsKey(height), bz) + // TODO: batch + s.db.Delete(s.shKey(height)) + s.db.Delete(s.vsKey(height + 1)) + return nil } +// SignedHeader loads SignedHeader at the given height. func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) { + if height <= 0 { + panic("negative or zero height") + } + bz := s.db.Get(s.shKey(height)) if bz == nil { return nil, nil @@ -66,7 +83,12 @@ func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) { return signedHeader, err } +// ValidatorSet loads ValidatorSet at the given height. func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) { + if height <= 0 { + panic("negative or zero height") + } + bz := s.db.Get(s.vsKey(height)) if bz == nil { return nil, nil @@ -77,6 +99,7 @@ func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) { return valSet, err } +// LastSignedHeaderHeight returns the last SignedHeader height stored. func (s *dbs) LastSignedHeaderHeight() (int64, error) { itr := s.db.ReverseIterator( s.shKey(1), @@ -90,20 +113,41 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) { if ok { return height, nil } + itr.Next() + } + + return -1, nil +} + +// FirstSignedHeaderHeight returns the first SignedHeader height stored. +func (s *dbs) FirstSignedHeaderHeight() (int64, error) { + itr := s.db.Iterator( + s.shKey(1), + append(s.shKey(1<<63-1), byte(0x00)), + ) + defer itr.Close() + + for itr.Valid() { + key := itr.Key() + _, height, ok := parseShKey(key) + if ok { + return height, nil + } + itr.Next() } - return -1, errors.New("no headers found") + return -1, nil } func (s *dbs) shKey(height int64) []byte { - return []byte(fmt.Sprintf("sh/%s/%010d", s.prefix, height)) + return []byte(fmt.Sprintf("sh/%s/%020d", s.prefix, height)) } func (s *dbs) vsKey(height int64) []byte { - return []byte(fmt.Sprintf("vs/%s/%010d", s.prefix, height)) + return []byte(fmt.Sprintf("vs/%s/%020d", s.prefix, height)) } -var keyPattern = regexp.MustCompile(`^(sh|vs)/([^/]*)/([0-9]+)/$`) +var keyPattern = regexp.MustCompile(`^(sh|vs)/([^/]*)/([0-9]+)$`) func parseKey(key []byte) (part string, prefix string, height int64, ok bool) { submatch := keyPattern.FindSubmatch(key) @@ -112,12 +156,10 @@ func parseKey(key []byte) (part string, prefix string, height int64, ok bool) { } part = string(submatch[1]) prefix = string(submatch[2]) - heightStr := string(submatch[3]) - heightInt, err := strconv.Atoi(heightStr) + height, err := strconv.ParseInt(string(submatch[3]), 10, 64) if err != nil { return "", "", 0, false } - height = int64(heightInt) ok = true // good! return } diff --git a/lite2/store/db/db_test.go b/lite2/store/db/db_test.go new file mode 100644 index 000000000..9edc1b2d9 --- /dev/null +++ b/lite2/store/db/db_test.go @@ -0,0 +1,76 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/tendermint/tm-db" + + "github.com/tendermint/tendermint/types" +) + +func TestLast_FirstSignedHeaderHeight(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "TestLast_FirstSignedHeaderHeight") + + // Empty store + height, err := dbStore.LastSignedHeaderHeight() + require.NoError(t, err) + assert.EqualValues(t, -1, height) + + height, err = dbStore.FirstSignedHeaderHeight() + require.NoError(t, err) + assert.EqualValues(t, -1, height) + + // 1 key + err = dbStore.SaveSignedHeaderAndNextValidatorSet( + &types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{}) + require.NoError(t, err) + + height, err = dbStore.LastSignedHeaderHeight() + require.NoError(t, err) + assert.EqualValues(t, 1, height) + + height, err = dbStore.FirstSignedHeaderHeight() + require.NoError(t, err) + assert.EqualValues(t, 1, height) +} + +func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "Test_SaveSignedHeaderAndNextValidatorSet") + + // Empty store + h, err := dbStore.SignedHeader(1) + require.NoError(t, err) + assert.Nil(t, h) + + valSet, err := dbStore.ValidatorSet(2) + require.NoError(t, err) + assert.Nil(t, valSet) + + // 1 key + err = dbStore.SaveSignedHeaderAndNextValidatorSet( + &types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{}) + require.NoError(t, err) + + h, err = dbStore.SignedHeader(1) + require.NoError(t, err) + assert.NotNil(t, h) + + valSet, err = dbStore.ValidatorSet(2) + require.NoError(t, err) + assert.NotNil(t, valSet) + + // Empty store + err = dbStore.DeleteSignedHeaderAndNextValidatorSet(1) + require.NoError(t, err) + + h, err = dbStore.SignedHeader(1) + require.NoError(t, err) + assert.Nil(t, h) + + valSet, err = dbStore.ValidatorSet(2) + require.NoError(t, err) + assert.Nil(t, valSet) +} diff --git a/lite2/store/store.go b/lite2/store/store.go index fb52ad032..cdcc25fa5 100644 --- a/lite2/store/store.go +++ b/lite2/store/store.go @@ -4,15 +4,17 @@ import "github.com/tendermint/tendermint/types" // Store is anything that can persistenly store headers. type Store interface { - // SaveSignedHeader saves a SignedHeader. + // SaveSignedHeaderAndNextValidatorSet saves a SignedHeader (h: sh.Height) + // and a ValidatorSet (h: sh.Height+1). // // height must be > 0. - SaveSignedHeader(sh *types.SignedHeader) error + SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error - // SaveValidatorSet saves a ValidatorSet. + // DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader (h: height) and + // ValidatorSet (h: height+1). // // height must be > 0. - SaveValidatorSet(valSet *types.ValidatorSet, height int64) error + DeleteSignedHeaderAndNextValidatorSet(height int64) error // SignedHeader returns the SignedHeader that corresponds to the given // height. @@ -31,8 +33,13 @@ type Store interface { // is returned. ValidatorSet(height int64) (*types.ValidatorSet, error) - // LastSignedHeaderHeight returns the last SignedHeader height. + // LastSignedHeaderHeight returns the last (newest) SignedHeader height. // - // If the store is empty, an error is returned. + // If the store is empty, -1 and nil error are returned. LastSignedHeaderHeight() (int64, error) + + // FirstSignedHeaderHeight returns the first (oldest) SignedHeader height. + // + // If the store is empty, -1 and nil error are returned. + FirstSignedHeaderHeight() (int64, error) } diff --git a/lite2/test_helpers.go b/lite2/test_helpers.go index 912a407b2..872704294 100644 --- a/lite2/test_helpers.go +++ b/lite2/test_helpers.go @@ -5,7 +5,6 @@ import ( "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" @@ -29,34 +28,34 @@ func genPrivKeys(n int) privKeys { return res } -// Change replaces the key at index i. -func (pkz privKeys) Change(i int) privKeys { - res := make(privKeys, len(pkz)) - copy(res, pkz) - res[i] = ed25519.GenPrivKey() - return res -} - -// Extend adds n more keys (to remove, just take a slice). -func (pkz privKeys) Extend(n int) privKeys { - extra := genPrivKeys(n) - return append(pkz, extra...) -} - -// GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. -func GenSecpPrivKeys(n int) privKeys { - res := make(privKeys, n) - for i := range res { - res[i] = secp256k1.GenPrivKey() - } - return res -} - -// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice). -func (pkz privKeys) ExtendSecp(n int) privKeys { - extra := GenSecpPrivKeys(n) - return append(pkz, extra...) -} +// // Change replaces the key at index i. +// func (pkz privKeys) Change(i int) privKeys { +// res := make(privKeys, len(pkz)) +// copy(res, pkz) +// res[i] = ed25519.GenPrivKey() +// return res +// } + +// // Extend adds n more keys (to remove, just take a slice). +// func (pkz privKeys) Extend(n int) privKeys { +// extra := genPrivKeys(n) +// return append(pkz, extra...) +// } + +// // GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. +// func GenSecpPrivKeys(n int) privKeys { +// res := make(privKeys, n) +// for i := range res { +// res[i] = secp256k1.GenPrivKey() +// } +// return res +// } + +// // ExtendSecp adds n more secp256k1 keys (to remove, just take a slice). +// func (pkz privKeys) ExtendSecp(n int) privKeys { +// extra := GenSecpPrivKeys(n) +// return append(pkz, extra...) +// } // ToValidators produces a valset from the set of keys. // The first key has weight `init` and it increases by `inc` every step diff --git a/lite2/verifier.go b/lite2/verifier.go index 3737dbbf6..bc242c803 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -16,6 +16,15 @@ var ( DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3} ) +// Verify verifies the new header (h2) against the old header (h1). It ensures that: +// +// a) h1 can still be trusted (if not, ErrOldHeaderExpired is returned); +// b) h2 is valid (if not, ErrInvalidNewHeader is returned); +// c) either h2.ValidatorsHash equals h1NextVals.Hash() +// OR trustLevel ([1/3, 1]) of last trusted validators (h1NextVals) signed +// correctly (if not, ErrNewValSetCantBeTrusted is returned); +// c) more than 2/3 of new validators (h2Vals) have signed h2 (if not, +// ErrNotEnoughVotingPowerSigned is returned). func Verify( chainID string, h1 *types.SignedHeader, @@ -31,9 +40,8 @@ func Verify( } // Ensure last header can still be trusted. - expirationTime := h1.Time.Add(trustingPeriod) - if !expirationTime.After(now) { - return ErrOldHeaderExpired{expirationTime, now} + if HeaderExpired(h1, trustingPeriod, now) { + return ErrOldHeaderExpired{h1.Time.Add(trustingPeriod), now} } if err := verifyNewHeaderAndVals(chainID, h2, h2Vals, h1, now); err != nil { @@ -42,16 +50,22 @@ func Verify( if h2.Height == h1.Height+1 { if !bytes.Equal(h2.ValidatorsHash, h1NextVals.Hash()) { - return errors.Errorf("expected old header validators (%X) to match those from new header (%X)", + err := errors.Errorf("expected old header next validators (%X) to match those from new header (%X)", h1NextVals.Hash(), h2.ValidatorsHash, ) + return err } } else { // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. err := h1NextVals.VerifyCommitTrusting(chainID, h2.Commit.BlockID, h2.Height, h2.Commit, trustLevel) if err != nil { - return err + switch e := err.(type) { + case types.ErrNotEnoughVotingPowerSigned: + return ErrNewValSetCantBeTrusted{e} + default: + return e + } } } @@ -95,8 +109,8 @@ func verifyNewHeaderAndVals( if !bytes.Equal(h2.ValidatorsHash, h2Vals.Hash()) { return errors.Errorf("expected new header validators (%X) to match those that were supplied (%X)", + h2.ValidatorsHash, h2Vals.Hash(), - h2.NextValidatorsHash, ) } @@ -114,3 +128,9 @@ func ValidateTrustLevel(lvl tmmath.Fraction) error { } return nil } + +// HeaderExpired return true if the given header expired. +func HeaderExpired(h *types.SignedHeader, trustingPeriod time.Duration, now time.Time) bool { + expirationTime := h.Time.Add(trustingPeriod) + return !expirationTime.After(now) +} diff --git a/lite2/verifier_test.go b/lite2/verifier_test.go index 321e9bdc3..1625213cc 100644 --- a/lite2/verifier_test.go +++ b/lite2/verifier_test.go @@ -11,9 +11,9 @@ import ( "github.com/tendermint/tendermint/types" ) -func TestVerifyAdjustedHeaders(t *testing.T) { +func TestVerifyAdjacentHeaders(t *testing.T) { const ( - chainID = "TestVerifyAdjustedHeaders" + chainID = "TestVerifyAdjacentHeaders" lastHeight = 1 nextHeight = 2 ) @@ -52,10 +52,30 @@ func TestVerifyAdjustedHeaders(t *testing.T) { 3 * time.Hour, bTime.Add(2 * time.Hour), nil, - "h2.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not 'TestVerifyAdjustedHeaders'", + "h2.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not 'TestVerifyAdjacentHeaders'", }, - // 3/3 signed -> no error + // new header's time is before old header's time -> error 2: { + keys.GenSignedHeader(chainID, nextHeight, bTime.Add(-1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + vals, + 3 * time.Hour, + bTime.Add(2 * time.Hour), + nil, + "to be after old header time", + }, + // new header's time is from the future -> error + 3: { + keys.GenSignedHeader(chainID, nextHeight, bTime.Add(3*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + vals, + 3 * time.Hour, + bTime.Add(2 * time.Hour), + nil, + "new header has a time from the future", + }, + // 3/3 signed -> no error + 4: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), vals, @@ -65,7 +85,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) { "", }, // 2/3 signed -> no error - 3: { + 5: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 1, len(keys)), vals, @@ -75,17 +95,17 @@ func TestVerifyAdjustedHeaders(t *testing.T) { "", }, // 1/3 signed -> error - 4: { + 6: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), len(keys)-1, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), - types.ErrTooMuchChange{Got: 50, Needed: 93}, + types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}, "", }, // vals does not match with what we have -> error - 5: { + 7: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, keys.ToValidators(10, 1), vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), @@ -95,7 +115,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) { "to match those from new header", }, // vals are inconsistent with newHeader -> error - 6: { + 8: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), @@ -105,7 +125,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) { "to match those that were supplied", }, // old header has expired -> error - 7: { + 9: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), @@ -131,11 +151,12 @@ func TestVerifyAdjustedHeaders(t *testing.T) { } }) } + } -func TestVerifyNonAdjustedHeaders(t *testing.T) { +func TestVerifyNonAdjacentHeaders(t *testing.T) { const ( - chainID = "TestVerifyNonAdjustedHeaders" + chainID = "TestVerifyNonAdjacentHeaders" lastHeight = 1 ) @@ -195,7 +216,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) { vals, 3 * time.Hour, bTime.Add(2 * time.Hour), - types.ErrTooMuchChange{Got: 50, Needed: 93}, + types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}, "", }, // 3/3 new vals signed, 2/3 old vals present -> no error @@ -225,7 +246,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) { lessThanOneThirdVals, 3 * time.Hour, bTime.Add(2 * time.Hour), - types.ErrTooMuchChange{Got: 20, Needed: 46}, + ErrNewValSetCantBeTrusted{types.ErrNotEnoughVotingPowerSigned{Got: 20, Needed: 46}}, "", }, } @@ -247,6 +268,26 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) { } } +func TestVerifyReturnsErrorIfTrustLevelIsInvalid(t *testing.T) { + const ( + chainID = "TestVerifyReturnsErrorIfTrustLevelIsInvalid" + lastHeight = 1 + ) + + 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, lastHeight, bTime, nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + ) + + err := Verify(chainID, header, vals, header, vals, 2*time.Hour, time.Now(), + tmmath.Fraction{Numerator: 2, Denominator: 1}) + assert.Error(t, err) +} + func TestValidateTrustLevel(t *testing.T) { testCases := []struct { lvl tmmath.Fraction diff --git a/types/validator_set.go b/types/validator_set.go index 29859a48a..390df09cf 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -631,7 +631,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, if vals.Size() != len(commit.Signatures) { return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures)) } - if err := vals.verifyCommitBasic(commit, height, blockID); err != nil { + if err := verifyCommitBasic(commit, height, blockID); err != nil { return err } @@ -661,7 +661,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, } if got, needed := talliedVotingPower, vals.TotalVotingPower()*2/3; got <= needed { - return ErrTooMuchChange{Got: got, Needed: needed} + return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} } return nil @@ -738,7 +738,7 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin } if got, needed := oldVotingPower, oldVals.TotalVotingPower()*2/3; got <= needed { - return ErrTooMuchChange{Got: got, Needed: needed} + return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} } return nil } @@ -755,11 +755,15 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID, panic(fmt.Sprintf("trustLevel must be within [1/3, 1], given %v", trustLevel)) } - if err := vals.verifyCommitBasic(commit, height, blockID); err != nil { + if err := verifyCommitBasic(commit, height, blockID); err != nil { return err } - talliedVotingPower := int64(0) + var ( + talliedVotingPower int64 + seenVals = make(map[int]int, len(commit.Signatures)) // validator index -> commit index + ) + for idx, commitSig := range commit.Signatures { if commitSig.Absent() { continue // OK, some signatures can be absent. @@ -767,8 +771,16 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID, // We don't know the validators that committed this block, so we have to // check for each vote if its validator is already known. - _, val := vals.GetByAddress(commitSig.ValidatorAddress) + valIdx, val := vals.GetByAddress(commitSig.ValidatorAddress) + + if firstIndex, ok := seenVals[valIdx]; ok { // double vote + secondIndex := idx + return errors.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex) + } + if val != nil { + seenVals[valIdx] = idx + // Validate signature. voteSignBytes := commit.VoteSignBytes(chainID, idx) if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) { @@ -789,13 +801,13 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID, got := talliedVotingPower needed := (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator if got <= needed { - return ErrTooMuchChange{Got: got, Needed: needed} + return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} } return nil } -func (vals *ValidatorSet) verifyCommitBasic(commit *Commit, height int64, blockID BlockID) error { +func verifyCommitBasic(commit *Commit, height int64, blockID BlockID) error { if err := commit.ValidateBasic(); err != nil { return err } @@ -810,23 +822,23 @@ func (vals *ValidatorSet) verifyCommitBasic(commit *Commit, height int64, blockI } //----------------- -// ErrTooMuchChange -// IsErrTooMuchChange returns true if err is related to changes in validator -// set exceeding max limit. -func IsErrTooMuchChange(err error) bool { - _, ok := errors.Cause(err).(ErrTooMuchChange) +// IsErrNotEnoughVotingPowerSigned returns true if err is +// ErrNotEnoughVotingPowerSigned. +func IsErrNotEnoughVotingPowerSigned(err error) bool { + _, ok := errors.Cause(err).(ErrNotEnoughVotingPowerSigned) return ok } -// ErrTooMuchChange indicates that changes in the validator set exceeded max limit. -type ErrTooMuchChange struct { +// ErrNotEnoughVotingPowerSigned is returned when not enough validators signed +// a commit. +type ErrNotEnoughVotingPowerSigned struct { Got int64 Needed int64 } -func (e ErrTooMuchChange) Error() string { - return fmt.Sprintf("invalid commit -- insufficient old voting power: got %d, needed more than %d", e.Got, e.Needed) +func (e ErrNotEnoughVotingPowerSigned) Error() string { + return fmt.Sprintf("invalid commit -- insufficient voting power: got %d, needed more than %d", e.Got, e.Needed) } //----------------