From 5c380cdacbcbd25461080a1d3724e8e1509f1766 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 31 Mar 2020 14:20:22 +0200 Subject: [PATCH] lite2: use bisection for some of backward verification (#4575) Closes: #4537 Uses SignedHeaderBefore to find header before unverified header and then bisection to verify the header. Only when header is between first and last trusted header height else if before the first trusted header height then regular backwards verification is used. --- CHANGELOG_PENDING.md | 1 + .../adr-046-light-client-implementation.md | 5 +- lite2/client.go | 141 +++++++++++------- lite2/client_benchmark_test.go | 18 +-- lite2/client_test.go | 62 ++++---- lite2/errors.go | 8 + lite2/store/db/db.go | 12 +- lite2/store/db/db_test.go | 10 +- lite2/store/store.go | 4 +- 9 files changed, 157 insertions(+), 104 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6aeb1007a..fe8f4a48e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -25,6 +25,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [Docker] \#4569 Default configuration added to docker image (you can still mount your own config the same way) (@greg-szabo) - [lite2] [\#4562](https://github.com/tendermint/tendermint/pull/4562) Cache headers when using bisection (@cmwaters) - [all] [\#4608](https://github.com/tendermint/tendermint/pull/4608) Give reactors descriptive names when they're initialized +- [lite2] [\#4575](https://github.com/tendermint/tendermint/pull/4575) Use bisection for within-range verification (@cmwaters) - [tools] \#4615 Allow developers to use Docker to generate proto stubs, via `make proto-gen-docker`. ### BUG FIXES: diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index 37a7c83c5..a1e322a9c 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -60,8 +60,9 @@ also cross-checked with witnesses for additional security. Due to bisection algorithm nature, some headers might be skipped. If the light client does not have a header for height `X` and `VerifyHeaderAtHeight(X)` or -`VerifyHeader(H#X)` methods are called, it will perform a backwards -verification from the latest header back to the header at height `X`. +`VerifyHeader(H#X)` methods are called, these will perform either a) backwards +verification from the latest header back to the header at height `X` or b) +bisection verification from the first stored header to the header at height `X`. `TrustedHeader`, `TrustedValidatorSet` only communicate with the trusted store. If some header is not there, an error will be returned indicating that diff --git a/lite2/client.go b/lite2/client.go index 14c07e511..b93c54b8b 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -149,7 +149,7 @@ func NewClient( options ...Option) (*Client, error) { if err := trustOptions.ValidateBasic(); err != nil { - return nil, errors.Wrap(err, "invalid TrustOptions") + return nil, fmt.Errorf("invalid TrustOptions: %w", err) } c, err := NewClientFromTrustedStore(chainID, trustOptions.Period, primary, witnesses, trustedStore, options...) @@ -206,13 +206,13 @@ func NewClientFromTrustedStore( // Validate the number of witnesses. if len(c.witnesses) < 1 { - return nil, errors.New("expected at least one witness") + return nil, errNoWitnesses{} } // Verify witnesses are all on the same chain. for i, w := range witnesses { if w.ChainID() != chainID { - return nil, errors.Errorf("witness #%d: %v is on another chain %s, expected %s", + return nil, fmt.Errorf("witness #%d: %v is on another chain %s, expected %s", i, w, w.ChainID(), chainID) } } @@ -234,18 +234,18 @@ func NewClientFromTrustedStore( func (c *Client) restoreTrustedHeaderAndVals() error { lastHeight, err := c.trustedStore.LastSignedHeaderHeight() if err != nil { - return errors.Wrap(err, "can't get last trusted header height") + return fmt.Errorf("can't get last trusted header height: %w", err) } if lastHeight > 0 { trustedHeader, err := c.trustedStore.SignedHeader(lastHeight) if err != nil { - return errors.Wrap(err, "can't get last trusted header") + return fmt.Errorf("can't get last trusted header: %w", err) } trustedVals, err := c.trustedStore.ValidatorSet(lastHeight) if err != nil { - return errors.Wrap(err, "can't get last trusted validators") + return fmt.Errorf("can't get last trusted validators: %w", err) } c.latestTrustedHeader = trustedHeader @@ -300,7 +300,7 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { // remove all the headers (options.Height, trustedHeader.Height] err := c.cleanupAfter(options.Height) if err != nil { - return errors.Wrapf(err, "cleanupAfter(%d)", options.Height) + return fmt.Errorf("cleanupAfter(%d): %w", options.Height, err) } c.logger.Info("Rolled back to older header (newer headers were removed)", @@ -322,7 +322,7 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { if c.confirmationFn(action) { err := c.Cleanup() if err != nil { - return errors.Wrap(err, "failed to cleanup") + return fmt.Errorf("failed to cleanup: %w", err) } } else { return errors.New("refused to remove the stored headers despite hashes mismatch") @@ -350,7 +350,7 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { } if !bytes.Equal(h.Hash(), options.Hash) { - return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash()) + return fmt.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash()) } err = c.compareNewHeaderWithWitnesses(h) @@ -365,7 +365,7 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { } if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { - return errors.Errorf("expected header's validators (%X) to match those that were supplied (%X)", + return fmt.Errorf("expected header's validators (%X) to match those that were supplied (%X)", h.ValidatorsHash, vals.Hash(), ) @@ -374,7 +374,7 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { // 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") + return fmt.Errorf("invalid commit: %w", err) } // 3) Persist both of them and continue. @@ -436,7 +436,7 @@ func (c *Client) TrustedValidatorSet(height int64) (valSet *types.ValidatorSet, func (c *Client) compareWithLatestHeight(height int64) (int64, error) { latestHeight, err := c.LastTrustedHeight() if err != nil { - return 0, errors.Wrap(err, "can't get last trusted height") + return 0, fmt.Errorf("can't get last trusted height: %w", err) } if latestHeight == -1 { return 0, errors.New("no headers exist") @@ -444,7 +444,7 @@ func (c *Client) compareWithLatestHeight(height int64) (int64, error) { switch { case height > latestHeight: - return 0, errors.Errorf("unverified header/valset requested (latest: %d)", latestHeight) + return 0, fmt.Errorf("unverified header/valset requested (latest: %d)", latestHeight) case height == 0: return latestHeight, nil case height < 0: @@ -499,6 +499,11 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // Intermediate headers are not saved to database. // https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md // +// If the header, which is older than the currently trusted header, is +// requested and the light client does not have it, VerifyHeader will perform: +// a) bisection verification if nearest trusted header is found & not expired +// b) backwards verification in all other cases +// // It returns ErrOldHeaderExpired if the latest trusted header expired. // // If the primary provides an invalid header (ErrInvalidHeader), it is rejected @@ -517,7 +522,7 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali if err == nil { // Make sure it's the same header. if !bytes.Equal(h.Hash(), newHeader.Hash()) { - return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) + return fmt.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) } c.logger.Info("Header has already been verified", "height", newHeader.Height, "hash", hash2str(newHeader.Hash())) @@ -533,7 +538,7 @@ func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.Vali var err error - // 1) If going forward, perform either bisection or sequential verification + // 1) If going forward, perform either bisection or sequential verification. if newHeader.Height >= c.latestTrustedHeader.Height { switch c.verificationMode { case sequential: @@ -544,26 +549,55 @@ func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.Vali panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) } } else { - // 2) Otherwise, perform backwards verification - // Find the closest trusted header after newHeader.Height - var closestHeader *types.SignedHeader - closestHeader, err = c.trustedStore.SignedHeaderAfter(newHeader.Height) + // 2) If verifying before the first trusted header, perform backwards + // verification. + var ( + closestHeader *types.SignedHeader + firstHeaderHeight int64 + ) + firstHeaderHeight, err = c.FirstTrustedHeight() if err != nil { - return errors.Wrapf(err, "can't get signed header after height %d", newHeader.Height) + return fmt.Errorf("can't get first header height: %w", err) + } + if newHeader.Height < firstHeaderHeight { + closestHeader, err = c.TrustedHeader(firstHeaderHeight) + if err != nil { + return fmt.Errorf("can't get first signed header: %w", err) + } + if HeaderExpired(closestHeader, c.trustingPeriod, now) { + closestHeader = c.latestTrustedHeader + } + err = c.backwards(closestHeader, newHeader, now) + } else { + // 3) OR if between trusted headers where the nearest has not expired, + // perform bisection verification, else backwards. + closestHeader, err = c.trustedStore.SignedHeaderBefore(newHeader.Height) + if err != nil { + return fmt.Errorf("can't get signed header before height %d: %w", newHeader.Height, err) + } + var closestValidatorSet *types.ValidatorSet + if c.verificationMode == sequential || HeaderExpired(closestHeader, c.trustingPeriod, now) { + err = c.backwards(c.latestTrustedHeader, newHeader, now) + } else { + closestValidatorSet, _, err = c.TrustedValidatorSet(closestHeader.Height) + if err != nil { + return fmt.Errorf("can't get validator set at height %d: %w", closestHeader.Height, err) + } + err = c.bisection(closestHeader, closestValidatorSet, newHeader, newVals, now) + } } - - err = c.backwards(closestHeader, newHeader, now) } if err != nil { c.logger.Error("Can't verify", "err", err) return err } - + // 4) Compare header with other witnesses if err := c.compareNewHeaderWithWitnesses(newHeader); err != nil { c.logger.Error("Error when comparing new header with witnesses", "err", err) return err } + // 5) Once verified, save and return return c.updateTrustedHeaderAndVals(newHeader, newVals) } @@ -590,7 +624,7 @@ func (c *Client) sequence( } else { // intermediate headers interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(height) if err != nil { - return errors.Wrapf(err, "failed to obtain the header #%d", height) + return err } } @@ -604,10 +638,10 @@ func (c *Client) sequence( err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, interimVals, c.trustingPeriod, now) if err != nil { - err = errors.Wrapf(err, "verify adjacent from #%d to #%d failed", - trustedHeader.Height, interimHeader.Height) + err = fmt.Errorf("verify adjacent from #%d to #%d failed: %w", + trustedHeader.Height, interimHeader.Height, err) - switch errors.Cause(err).(type) { + switch errors.Unwrap(err).(type) { case ErrInvalidHeader: c.logger.Error("primary sent invalid header -> replacing", "err", err) replaceErr := c.replacePrimaryProvider() @@ -696,15 +730,15 @@ func (c *Client) bisection( if replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) // return original error - return errors.Wrapf(err, "verify from #%d to #%d failed", - trustedHeader.Height, headerCache[depth].sh.Height) + return fmt.Errorf("verify non adjacent from #%d to #%d failed: %w", + trustedHeader.Height, headerCache[depth].sh.Height, err) } // attempt to verify the header again continue default: - return errors.Wrapf(err, "verify from #%d to #%d failed", - trustedHeader.Height, headerCache[depth].sh.Height) + return fmt.Errorf("verify non adjacent from #%d to #%d failed: %w", + trustedHeader.Height, headerCache[depth].sh.Height, err) } } } @@ -762,14 +796,14 @@ func (c *Client) Cleanup() error { // cleanupAfter deletes all headers & validator sets after +height+. It also // resets latestTrustedHeader to the latest header. func (c *Client) cleanupAfter(height int64) error { - nextHeight := height + prevHeight := c.latestTrustedHeader.Height for { - h, err := c.trustedStore.SignedHeaderAfter(nextHeight) - if err == store.ErrSignedHeaderNotFound { + h, err := c.trustedStore.SignedHeaderBefore(prevHeight) + if err == store.ErrSignedHeaderNotFound || (h != nil && h.Height <= height) { break } else if err != nil { - return errors.Wrapf(err, "failed to get header after %d", nextHeight) + return fmt.Errorf("failed to get header before %d: %w", prevHeight, err) } err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(h.Height) @@ -778,7 +812,7 @@ func (c *Client) cleanupAfter(height int64) error { "height", h.Height) } - nextHeight = h.Height + prevHeight = h.Height } c.latestTrustedHeader = nil @@ -793,16 +827,16 @@ func (c *Client) cleanupAfter(height int64) error { func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { - return errors.Errorf("expected validator's hash %X, but got %X", h.ValidatorsHash, vals.Hash()) + return fmt.Errorf("expected validator's hash %X, but got %X", h.ValidatorsHash, vals.Hash()) } if err := c.trustedStore.SaveSignedHeaderAndValidatorSet(h, vals); err != nil { - return errors.Wrap(err, "failed to save trusted header") + return fmt.Errorf("failed to save trusted header: %w", err) } if c.pruningSize > 0 { if err := c.trustedStore.Prune(c.pruningSize); err != nil { - return errors.Wrap(err, "prune") + return fmt.Errorf("prune: %w", err) } } @@ -819,11 +853,11 @@ func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.V func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) { h, err := c.signedHeaderFromPrimary(height) if err != nil { - return nil, nil, errors.Wrapf(err, "failed to obtain the header #%d", height) + return nil, nil, fmt.Errorf("failed to obtain the header #%d: %w", height, err) } vals, err := c.validatorSetFromPrimary(height) if err != nil { - return nil, nil, errors.Wrapf(err, "failed to obtain the vals #%d", height) + return nil, nil, fmt.Errorf("failed to obtain the vals #%d: %w", height, err) } return h, vals, nil } @@ -837,6 +871,7 @@ func (c *Client) backwards( now time.Time) error { if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { + c.logger.Error("Header Expired") return ErrOldHeaderExpired{initiallyTrustedHeader.Time.Add(c.trustingPeriod), now} } @@ -849,16 +884,20 @@ func (c *Client) backwards( for trustedHeader.Height > newHeader.Height { interimHeader, err = c.signedHeaderFromPrimary(trustedHeader.Height - 1) if err != nil { - return errors.Wrapf(err, "failed to obtain the header at height #%d", trustedHeader.Height-1) + return fmt.Errorf("failed to obtain the header at height #%d: %w", trustedHeader.Height-1, err) } - + c.logger.Debug("Verify newHeader against trustedHeader", + "trustedHeight", trustedHeader.Height, + "trustedHash", hash2str(trustedHeader.Hash()), + "newHeight", interimHeader.Height, + "newHash", hash2str(interimHeader.Hash())) if err := VerifyBackwards(c.chainID, interimHeader, trustedHeader); err != nil { c.logger.Error("primary sent invalid header -> replacing", "err", err) if replaceErr := c.replacePrimaryProvider(); replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) // return original error - return errors.Wrapf(err, "verify backwards from %d to %d failed", - trustedHeader.Height, interimHeader.Height) + return fmt.Errorf("verify backwards from %d to %d failed: %w", + trustedHeader.Height, interimHeader.Height, err) } } @@ -883,7 +922,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { witnessesToRemove := make([]int, 0) for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { if len(c.witnesses) == 0 { - return errors.New("could not find any witnesses. please reset the light client") + return errNoWitnesses{} } for i, witness := range c.witnesses { @@ -909,7 +948,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { // TODO: send the diverged headers to primary && all witnesses - return errors.Errorf( + return fmt.Errorf( "header hash %X does not match one %X from the witness %v", h.Hash(), altH.Hash(), witness) } @@ -952,7 +991,7 @@ func (c *Client) removeWitness(idx int) { func (c *Client) Update(now time.Time) (*types.SignedHeader, error) { lastTrustedHeight, err := c.LastTrustedHeight() if err != nil { - return nil, errors.Wrap(err, "can't get last trusted height") + return nil, fmt.Errorf("can't get last trusted height: %w", err) } if lastTrustedHeight == -1 { @@ -962,7 +1001,7 @@ func (c *Client) Update(now time.Time) (*types.SignedHeader, error) { latestHeader, latestVals, err := c.fetchHeaderAndValsAtHeight(0) if err != nil { - return nil, errors.Wrapf(err, "can't get latest header and vals") + return nil, err } if latestHeader.Height > lastTrustedHeight { @@ -984,7 +1023,7 @@ func (c *Client) replacePrimaryProvider() error { defer c.providerMutex.Unlock() if len(c.witnesses) <= 1 { - return errors.Errorf("only one witness left. please reset the light client") + return errNoWitnesses{} } c.primary = c.witnesses[0] c.witnesses = c.witnesses[1:] @@ -1004,7 +1043,7 @@ func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, err if err == nil { // sanity check if height > 0 && h.Height != height { - return nil, errors.Errorf("expected %d height, got %d", height, h.Height) + return nil, fmt.Errorf("expected %d height, got %d", height, h.Height) } return h, nil } diff --git a/lite2/client_benchmark_test.go b/lite2/client_benchmark_test.go index 276c962a1..97b140ea5 100644 --- a/lite2/client_benchmark_test.go +++ b/lite2/client_benchmark_test.go @@ -20,8 +20,8 @@ import ( // // Remember that none of these benchmarks account for network latency. var ( - largeFullNode = mockp.New(GenMockNode(chainID, 1000, 100, 1, bTime)) - genesisHeader, _ = largeFullNode.SignedHeader(1) + benchmarkFullNode = mockp.New(GenMockNode(chainID, 1000, 100, 1, bTime)) + genesisHeader, _ = benchmarkFullNode.SignedHeader(1) ) func BenchmarkSequence(b *testing.B) { @@ -32,8 +32,8 @@ func BenchmarkSequence(b *testing.B) { Height: 1, Hash: genesisHeader.Hash(), }, - largeFullNode, - []provider.Provider{largeFullNode}, + benchmarkFullNode, + []provider.Provider{benchmarkFullNode}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), SequentialVerification(), @@ -59,8 +59,8 @@ func BenchmarkBisection(b *testing.B) { Height: 1, Hash: genesisHeader.Hash(), }, - largeFullNode, - []provider.Provider{largeFullNode}, + benchmarkFullNode, + []provider.Provider{benchmarkFullNode}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) @@ -78,7 +78,7 @@ func BenchmarkBisection(b *testing.B) { } func BenchmarkBackwards(b *testing.B) { - trustedHeader, _ := largeFullNode.SignedHeader(0) + trustedHeader, _ := benchmarkFullNode.SignedHeader(0) c, err := NewClient( chainID, TrustOptions{ @@ -86,8 +86,8 @@ func BenchmarkBackwards(b *testing.B) { Height: trustedHeader.Height, Hash: trustedHeader.Hash(), }, - largeFullNode, - []provider.Provider{largeFullNode}, + benchmarkFullNode, + []provider.Provider{benchmarkFullNode}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) diff --git a/lite2/client_test.go b/lite2/client_test.go index 65ea55122..9481fb0b5 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -57,7 +57,8 @@ var ( headerSet, valSet, ) - deadNode = mockp.NewDeadMock(chainID) + deadNode = mockp.NewDeadMock(chainID) + largeFullNode = mockp.New(GenMockNode(chainID, 10, 3, 0, bTime)) ) func TestClient_SequentialVerification(t *testing.T) { @@ -682,62 +683,65 @@ func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { func TestClient_BackwardsVerification(t *testing.T) { { + trustHeader, _ := largeFullNode.SignedHeader(6) c, err := NewClient( chainID, TrustOptions{ - Period: 1 * time.Hour, - Height: 3, - Hash: h3.Hash(), + Period: 4 * time.Minute, + Height: trustHeader.Height, + Hash: trustHeader.Hash(), }, - fullNode, - []provider.Provider{fullNode}, + largeFullNode, + []provider.Provider{largeFullNode}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) require.NoError(t, err) - // 1) header is missing => expect no error - h, err := c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + // 1) verify before the trusted header using backwards => expect no error + h, err := c.VerifyHeaderAtHeight(5, bTime.Add(6*time.Minute)) require.NoError(t, err) if assert.NotNil(t, h) { - assert.EqualValues(t, 2, h.Height) + assert.EqualValues(t, 5, h.Height) } // 2) untrusted header is expired but trusted header is not => expect no error - h, err = c.VerifyHeaderAtHeight(1, bTime.Add(1*time.Hour).Add(1*time.Second)) + h, err = c.VerifyHeaderAtHeight(3, bTime.Add(8*time.Minute)) assert.NoError(t, err) assert.NotNil(t, h) // 3) already stored headers should return the header without error - h, err = c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + h, err = c.VerifyHeaderAtHeight(5, bTime.Add(6*time.Minute)) assert.NoError(t, err) assert.NotNil(t, h) - } - { - c, err := NewClient( - chainID, - TrustOptions{ - Period: 1 * time.Hour, - Height: 3, - Hash: h3.Hash(), - }, - fullNode, - []provider.Provider{fullNode}, - dbs.New(dbm.NewMemDB(), chainID), - Logger(log.TestingLogger()), - ) + + // 4a) First verify latest header + _, err = c.VerifyHeaderAtHeight(9, bTime.Add(9*time.Minute)) require.NoError(t, err) - // 3) trusted header has expired => expect error - _, err = c.VerifyHeaderAtHeight(1, bTime.Add(4*time.Hour).Add(1*time.Second)) + // 4b) Verify backwards using bisection => expect no error + _, err = c.VerifyHeaderAtHeight(7, bTime.Add(10*time.Minute)) + assert.NoError(t, err) + // shouldn't have verified this header in the process + _, err = c.TrustedHeader(8) assert.Error(t, err) + + // 5) trusted header has expired => expect error + _, err = c.VerifyHeaderAtHeight(1, bTime.Add(20*time.Minute)) + assert.Error(t, err) + + // 6) Try bisection method, but closest header (at 7) has expired + // so change to backwards => expect no error + _, err = c.VerifyHeaderAtHeight(8, bTime.Add(12*time.Minute)) + assert.NoError(t, err) + } { testCases := []struct { provider provider.Provider }{ { - // provides incorrect height + // 7) provides incorrect height mockp.New( chainID, map[int64]*types.SignedHeader{ @@ -750,7 +754,7 @@ func TestClient_BackwardsVerification(t *testing.T) { ), }, { - // provides incorrect hash + // 8) provides incorrect hash mockp.New( chainID, map[int64]*types.SignedHeader{ diff --git a/lite2/errors.go b/lite2/errors.go index 13a6cf29d..7bc70f698 100644 --- a/lite2/errors.go +++ b/lite2/errors.go @@ -38,3 +38,11 @@ type ErrInvalidHeader struct { func (e ErrInvalidHeader) Error() string { return fmt.Sprintf("invalid header: %v", e.Reason) } + +// errNoWitnesses means that there are not enough witnesses connected to +// continue running the light client. +type errNoWitnesses struct{} + +func (e errNoWitnesses) Error() string { + return fmt.Sprint("no witnesses connected. please reset light client") +} diff --git a/lite2/store/db/db.go b/lite2/store/db/db.go index d405b9865..8d37ace9f 100644 --- a/lite2/store/db/db.go +++ b/lite2/store/db/db.go @@ -203,18 +203,18 @@ func (s *dbs) FirstSignedHeaderHeight() (int64, error) { return -1, nil } -// SignedHeaderAfter iterates over headers until it finds a header after one at -// height. It returns ErrSignedHeaderNotFound if no such header exists. +// SignedHeaderBefore iterates over headers until it finds a header before +// the given height. It returns ErrSignedHeaderNotFound if no such header exists. // // Safe for concurrent use by multiple goroutines. -func (s *dbs) SignedHeaderAfter(height int64) (*types.SignedHeader, error) { +func (s *dbs) SignedHeaderBefore(height int64) (*types.SignedHeader, error) { if height <= 0 { panic("negative or zero height") } - itr, err := s.db.Iterator( - s.shKey(height+1), - append(s.shKey(1<<63-1), byte(0x00)), + itr, err := s.db.ReverseIterator( + s.shKey(1), + s.shKey(height), ) if err != nil { panic(err) diff --git a/lite2/store/db/db_test.go b/lite2/store/db/db_test.go index 2b82de8f3..ce45f3bcf 100644 --- a/lite2/store/db/db_test.go +++ b/lite2/store/db/db_test.go @@ -76,19 +76,19 @@ func Test_SaveSignedHeaderAndValidatorSet(t *testing.T) { assert.Nil(t, valSet) } -func Test_SignedHeaderAfter(t *testing.T) { - dbStore := New(dbm.NewMemDB(), "Test_SignedHeaderAfter") +func Test_SignedHeaderBefore(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "Test_SignedHeaderBefore") assert.Panics(t, func() { - dbStore.SignedHeaderAfter(0) - dbStore.SignedHeaderAfter(100) + _, _ = dbStore.SignedHeaderBefore(0) + _, _ = dbStore.SignedHeaderBefore(100) }) err := dbStore.SaveSignedHeaderAndValidatorSet( &types.SignedHeader{Header: &types.Header{Height: 2}}, &types.ValidatorSet{}) require.NoError(t, err) - h, err := dbStore.SignedHeaderAfter(1) + h, err := dbStore.SignedHeaderBefore(3) require.NoError(t, err) if assert.NotNil(t, h) { assert.EqualValues(t, 2, h.Height) diff --git a/lite2/store/store.go b/lite2/store/store.go index 7ea6b9c6b..0d36c48b6 100644 --- a/lite2/store/store.go +++ b/lite2/store/store.go @@ -41,10 +41,10 @@ type Store interface { // If the store is empty, -1 and nil error are returned. FirstSignedHeaderHeight() (int64, error) - // SignedHeaderAfter returns the SignedHeader after the certain height. + // SignedHeaderBefore returns the SignedHeader before a certain height. // // height must be > 0 && <= LastSignedHeaderHeight. - SignedHeaderAfter(height int64) (*types.SignedHeader, error) + SignedHeaderBefore(height int64) (*types.SignedHeader, error) // Prune removes headers & the associated validator sets when Store reaches a // defined size (number of header & validator set pairs).