From b1dba352b08ce907e5a6b29f2e41838d3f57be86 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Wed, 10 Jun 2020 18:56:24 +0200 Subject: [PATCH] light: added more tests for pruning, initialization and bisection (#4978) --- CHANGELOG_PENDING.md | 1 + light/client.go | 22 ++++-- light/client_test.go | 156 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 164 insertions(+), 15 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index e424bfbc9..acde88167 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -81,6 +81,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [rpc] [\#4532](https://github.com/tendermint/tendermint/pull/4923) Support `BlockByHash` query (@fedekunze) - [rpc] \#4979 Support EXISTS operator in `/tx_search` query (@melekes) - [p2p] \#4981 Expose `SaveAs` func on NodeKey (@melekes) +- [evidence] [#4821](https://github.com/tendermint/tendermint/pull/4821) Amnesia evidence can be detected, verified and committed (@cmwaters) ### IMPROVEMENTS: diff --git a/light/client.go b/light/client.go index 7ac138c76..8c9e1e2e6 100644 --- a/light/client.go +++ b/light/client.go @@ -477,6 +477,8 @@ func (c *Client) compareWithLatestHeight(height int64) (int64, error) { // // It returns provider.ErrSignedHeaderNotFound if header is not found by // primary. +// +// It will replace the primary provider if an error from a request to the provider occurs func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { if height <= 0 { return nil, errors.New("negative or zero height") @@ -639,7 +641,7 @@ func (c *Client) sequence( } // 2) Verify them - c.logger.Debug("Verify newHeader against trustedHeader", + c.logger.Debug("Verify adjacent newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", interimHeader.Height, @@ -648,7 +650,7 @@ func (c *Client) sequence( err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, interimVals, c.trustingPeriod, now, c.maxClockDrift) if err != nil { - err = fmt.Errorf("verify adjacent from #%d to #%d failed: %w", + err := fmt.Errorf("verify adjacent from #%d to #%d failed: %w", trustedHeader.Height, interimHeader.Height, err) switch errors.Unwrap(err).(type) { @@ -657,7 +659,7 @@ func (c *Client) sequence( replaceErr := c.replacePrimaryProvider() if replaceErr != nil { c.logger.Error("Can't replace primary", "err", replaceErr) - return err // return original error + return fmt.Errorf("%v. Tried to replace primary but: %w", err.Error(), replaceErr) } // attempt to verify header again height-- @@ -700,7 +702,7 @@ func (c *Client) bisection( ) for { - c.logger.Debug("Verify newHeader against trustedHeader", + c.logger.Debug("Verify non-adjacent newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", headerCache[depth].sh.Height, @@ -752,8 +754,18 @@ func (c *Client) bisection( return fmt.Errorf("verify non adjacent from #%d to #%d failed: %w", trustedHeader.Height, headerCache[depth].sh.Height, err) } + newProviderHeader, newProviderVals, err := c.fetchHeaderAndValsAtHeight(newHeader.Height) + if err != nil { + return err + } + if !bytes.Equal(newProviderHeader.Hash(), newHeader.Hash()) || !bytes.Equal(newProviderVals.Hash(), newVals.Hash()) { + err := fmt.Errorf("replacement provider has a different header: %X and/or vals: %X at height: %d"+ + "to the one being verified", newProviderHeader.Hash(), newProviderVals.Hash(), newHeader.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 + return c.bisection(initiallyTrustedHeader, initiallyTrustedVals, newHeader, newVals, now) default: return fmt.Errorf("verify non adjacent from #%d to #%d failed: %w", diff --git a/light/client_test.go b/light/client_test.go index 19851d6e5..215e03704 100644 --- a/light/client_test.go +++ b/light/client_test.go @@ -62,6 +62,52 @@ var ( largeFullNode = mockp.New(GenMockNode(chainID, 10, 3, 0, bTime)) ) +func TestValidateTrustOptions(t *testing.T) { + testCases := []struct { + err bool + to light.TrustOptions + }{ + { + false, + trustOptions, + }, + { + true, + light.TrustOptions{ + Period: -1 * time.Hour, + Height: 1, + Hash: h1.Hash(), + }, + }, + { + true, + light.TrustOptions{ + Period: 1 * time.Hour, + Height: 0, + Hash: h1.Hash(), + }, + }, + { + true, + light.TrustOptions{ + Period: 1 * time.Hour, + Height: 1, + Hash: []byte("incorrect hash"), + }, + }, + } + + for _, tc := range testCases { + err := tc.to.ValidateBasic() + if tc.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } + +} + func TestClient_SequentialVerification(t *testing.T) { newKeys := genPrivKeys(4) newVals := newKeys.ToValidators(10, 1) @@ -296,8 +342,11 @@ func TestClient_SkippingVerification(t *testing.T) { }) } - // start from a large header to make sure that the pivot height doesn't select a height outside - // the appropriate range +} + +// start from a large header to make sure that the pivot height doesn't select a height outside +// the appropriate range +func TestClientLargeBisectionVerification(t *testing.T) { veryLargeFullNode := mockp.New(GenMockNode(chainID, 100, 3, 1, bTime)) h1, err := veryLargeFullNode.SignedHeader(90) require.NoError(t, err) @@ -321,6 +370,34 @@ func TestClient_SkippingVerification(t *testing.T) { assert.Equal(t, h, h2) } +func TestClientBisectionBetweenTrustedHeaders(t *testing.T) { + c, err := light.NewClient( + chainID, + light.TrustOptions{ + Period: 4 * time.Hour, + Height: 1, + Hash: h1.Hash(), + }, + fullNode, + []provider.Provider{fullNode}, + dbs.New(dbm.NewMemDB(), chainID), + light.SkippingVerification(light.DefaultTrustLevel), + ) + require.NoError(t, err) + + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(2*time.Hour)) + require.NoError(t, err) + + // confirm that the client already doesn't have the header + _, err = c.TrustedHeader(2) + require.Error(t, err) + + // verify using bisection the header between the two trusted headers + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour)) + assert.NoError(t, err) + +} + func TestClient_Cleanup(t *testing.T) { c, err := light.NewClient( chainID, @@ -514,12 +591,11 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { + // load the first three headers into the trusted store trustedStore := dbs.New(dbm.NewMemDB(), chainID) err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, 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.SaveSignedHeaderAndValidatorSet(h2, vals) require.NoError(t, err) @@ -554,6 +630,10 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { valSet, _, err = c.TrustedValidatorSet(2) assert.Error(t, err) assert.Nil(t, valSet) + + h, err = c.TrustedHeader(3) + assert.Error(t, err) + assert.Nil(t, h) } // 2. options.Hash != trustedHeader.Hash @@ -907,26 +987,60 @@ func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { // header should still be verified assert.EqualValues(t, 2, h.Height) - // no witnesses left to verify -> error + // remaining withness doesn't have header -> error _, err = c.VerifyHeaderAtHeight(3, bTime.Add(2*time.Hour)) - assert.Error(t, err) + if assert.Error(t, err) { + assert.Equal(t, "awaiting response from all witnesses exceeded dropout time", err.Error()) + } assert.EqualValues(t, 0, len(c.Witnesses())) + + // no witnesses left, will not be allowed to verify a header + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(2*time.Hour)) + if assert.Error(t, err) { + assert.Equal(t, "no witnesses connected. please reset light client", err.Error()) + } } func TestClientTrustedValidatorSet(t *testing.T) { + noValSetNode := mockp.New( + chainID, + headerSet, + map[int64]*types.ValidatorSet{ + 1: nil, + 2: nil, + 3: nil, + }, + ) + + differentVals, _ := types.RandValidatorSet(10, 100) + + badValSetNode := mockp.New( + chainID, + headerSet, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: differentVals, + 3: differentVals, + }, + ) + c, err := light.NewClient( chainID, trustOptions, - fullNode, - []provider.Provider{fullNode}, + noValSetNode, + []provider.Provider{badValSetNode, fullNode, fullNode}, dbs.New(dbm.NewMemDB(), chainID), light.Logger(log.TestingLogger()), ) - require.NoError(t, err) + assert.Equal(t, 2, len(c.Witnesses())) _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second)) - require.NoError(t, err) + assert.Error(t, err) + assert.Equal(t, 1, len(c.Witnesses())) + + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second)) + assert.NoError(t, err) valSet, height, err := c.TrustedValidatorSet(0) assert.NoError(t, err) @@ -974,6 +1088,28 @@ func TestClientReportsConflictingHeadersEvidence(t *testing.T) { assert.True(t, fullNode.HasEvidence(ev)) } +func TestClientPrunesHeadersAndValidatorSets(t *testing.T) { + c, err := light.NewClient( + chainID, + trustOptions, + fullNode, + []provider.Provider{fullNode}, + dbs.New(dbm.NewMemDB(), chainID), + light.Logger(log.TestingLogger()), + light.PruningSize(1), + ) + require.NoError(t, err) + _, err = c.TrustedHeader(1) + require.NoError(t, err) + + h, err := c.Update(bTime.Add(2 * time.Hour)) + require.NoError(t, err) + require.Equal(t, int64(3), h.Height) + + _, err = c.TrustedHeader(1) + assert.Error(t, err) +} + func TestClientEnsureValidHeadersAndValSets(t *testing.T) { emptyValSet := &types.ValidatorSet{ Validators: nil,