- package light_test
-
- import (
- "context"
- "errors"
- "fmt"
- "sync"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
-
- dbm "github.com/tendermint/tm-db"
-
- "github.com/tendermint/tendermint/internal/test/factory"
- "github.com/tendermint/tendermint/libs/log"
- "github.com/tendermint/tendermint/light"
- "github.com/tendermint/tendermint/light/provider"
- provider_mocks "github.com/tendermint/tendermint/light/provider/mocks"
- dbs "github.com/tendermint/tendermint/light/store/db"
- "github.com/tendermint/tendermint/types"
- )
-
- const (
- chainID = "test"
- )
-
- var (
- ctx = context.Background()
- keys = genPrivKeys(4)
- vals = keys.ToValidators(20, 10)
- bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
- h1 = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys))
- // 3/3 signed
- h2 = keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys), types.BlockID{Hash: h1.Hash()})
- // 3/3 signed
- h3 = keys.GenSignedHeaderLastBlockID(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys), types.BlockID{Hash: h2.Hash()})
- trustPeriod = 4 * time.Hour
- trustOptions = light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: h1.Hash(),
- }
- valSet = map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- 3: vals,
- 4: vals,
- }
- headerSet = map[int64]*types.SignedHeader{
- 1: h1,
- // interim header (3/3 signed)
- 2: h2,
- // last header (3/3 signed)
- 3: h3,
- }
- l1 = &types.LightBlock{SignedHeader: h1, ValidatorSet: vals}
- l2 = &types.LightBlock{SignedHeader: h2, ValidatorSet: vals}
- l3 = &types.LightBlock{SignedHeader: h3, ValidatorSet: vals}
- )
-
- 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)
- differentVals, _ := factory.RandValidatorSet(10, 100)
-
- testCases := []struct {
- name string
- otherHeaders map[int64]*types.SignedHeader // all except ^
- vals map[int64]*types.ValidatorSet
- initErr bool
- verifyErr bool
- }{
- {
- "good",
- headerSet,
- valSet,
- false,
- false,
- },
- {
- "bad: different first header",
- map[int64]*types.SignedHeader{
- // different header
- 1: keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- },
- true,
- false,
- },
- {
- "bad: no first signed header",
- map[int64]*types.SignedHeader{},
- map[int64]*types.ValidatorSet{
- 1: differentVals,
- },
- true,
- true,
- },
- {
- "bad: different first validator set",
- map[int64]*types.SignedHeader{
- 1: h1,
- },
- map[int64]*types.ValidatorSet{
- 1: differentVals,
- },
- true,
- true,
- },
- {
- "bad: 1/3 signed interim header",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- // interim header (1/3 signed)
- 2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)),
- // last header (3/3 signed)
- 3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
- },
- valSet,
- false,
- true,
- },
- {
- "bad: 1/3 signed last header",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- // interim header (3/3 signed)
- 2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
- // last header (1/3 signed)
- 3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)),
- },
- valSet,
- false,
- true,
- },
- {
- "bad: different validator set at height 3",
- headerSet,
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- 3: newVals,
- },
- false,
- true,
- },
- }
-
- for _, tc := range testCases {
- testCase := tc
- t.Run(testCase.name, func(t *testing.T) {
- mockNode := mockNodeFromHeadersAndVals(testCase.otherHeaders, testCase.vals)
- mockNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockNode,
- []provider.Provider{mockNode},
- dbs.New(dbm.NewMemDB()),
- light.SequentialVerification(),
- light.Logger(log.TestingLogger()),
- )
-
- if testCase.initErr {
- require.Error(t, err)
- return
- }
-
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(3*time.Hour))
- if testCase.verifyErr {
- assert.Error(t, err)
- } else {
- assert.NoError(t, err)
- }
- mockNode.AssertExpectations(t)
- })
- }
- }
-
- func TestClient_SkippingVerification(t *testing.T) {
- // required for 2nd test case
- newKeys := genPrivKeys(4)
- newVals := newKeys.ToValidators(10, 1)
-
- // 1/3+ of vals, 2/3- of newVals
- transitKeys := keys.Extend(3)
- transitVals := transitKeys.ToValidators(10, 1)
-
- testCases := []struct {
- name string
- otherHeaders map[int64]*types.SignedHeader // all except ^
- vals map[int64]*types.ValidatorSet
- initErr bool
- verifyErr bool
- }{
- {
- "good",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- // last header (3/3 signed)
- 3: h3,
- },
- valSet,
- false,
- false,
- },
- {
- "good, but val set changes by 2/3 (1/3 of vals is still present)",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- 3: transitKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, transitVals, transitVals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(transitKeys)),
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- 3: transitVals,
- },
- false,
- false,
- },
- {
- "good, but val set changes 100% at height 2",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- // interim header (3/3 signed)
- 2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, newVals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
- // last header (0/4 of the original val set signed)
- 3: newKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, newVals, newVals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(newKeys)),
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- 3: newVals,
- },
- false,
- false,
- },
- {
- "bad: last header signed by newVals, interim header has no signers",
- map[int64]*types.SignedHeader{
- // trusted header
- 1: h1,
- // last header (0/4 of the original val set signed)
- 2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, newVals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, 0),
- // last header (0/4 of the original val set signed)
- 3: newKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, newVals, newVals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(newKeys)),
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- 3: newVals,
- },
- false,
- true,
- },
- }
-
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- mockNode := mockNodeFromHeadersAndVals(tc.otherHeaders, tc.vals)
- mockNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockNode,
- []provider.Provider{mockNode},
- dbs.New(dbm.NewMemDB()),
- light.SkippingVerification(light.DefaultTrustLevel),
- light.Logger(log.TestingLogger()),
- )
- if tc.initErr {
- require.Error(t, err)
- return
- }
-
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(3*time.Hour))
- if tc.verifyErr {
- assert.Error(t, err)
- } else {
- assert.NoError(t, err)
- }
- })
- }
-
- }
-
- // start from a large light block to make sure that the pivot height doesn't select a height outside
- // the appropriate range
- func TestClientLargeBisectionVerification(t *testing.T) {
- numBlocks := int64(300)
- mockHeaders, mockVals, _ := genLightBlocksWithKeys(chainID, numBlocks, 101, 2, bTime)
-
- lastBlock := &types.LightBlock{SignedHeader: mockHeaders[numBlocks], ValidatorSet: mockVals[numBlocks]}
- mockNode := &provider_mocks.Provider{}
- mockNode.On("LightBlock", mock.Anything, numBlocks).
- Return(lastBlock, nil)
-
- mockNode.On("LightBlock", mock.Anything, int64(200)).
- Return(&types.LightBlock{SignedHeader: mockHeaders[200], ValidatorSet: mockVals[200]}, nil)
-
- mockNode.On("LightBlock", mock.Anything, int64(256)).
- Return(&types.LightBlock{SignedHeader: mockHeaders[256], ValidatorSet: mockVals[256]}, nil)
-
- mockNode.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
-
- trustedLightBlock, err := mockNode.LightBlock(ctx, int64(200))
- require.NoError(t, err)
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: trustedLightBlock.Height,
- Hash: trustedLightBlock.Hash(),
- },
- mockNode,
- []provider.Provider{mockNode},
- dbs.New(dbm.NewMemDB()),
- light.SkippingVerification(light.DefaultTrustLevel),
- )
- require.NoError(t, err)
- h, err := c.Update(ctx, bTime.Add(300*time.Minute))
- assert.NoError(t, err)
- height, err := c.LastTrustedHeight()
- require.NoError(t, err)
- require.Equal(t, numBlocks, height)
- h2, err := mockNode.LightBlock(ctx, numBlocks)
- require.NoError(t, err)
- assert.Equal(t, h, h2)
- mockNode.AssertExpectations(t)
- }
-
- func TestClientBisectionBetweenTrustedHeaders(t *testing.T) {
- mockFullNode := mockNodeFromHeadersAndVals(headerSet, valSet)
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: h1.Hash(),
- },
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.SkippingVerification(light.DefaultTrustLevel),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
- require.NoError(t, err)
-
- // confirm that the client already doesn't have the light block
- _, err = c.TrustedLightBlock(2)
- require.Error(t, err)
-
- // verify using bisection the light block between the two trusted light blocks
- _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
- assert.NoError(t, err)
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClient_Cleanup(t *testing.T) {
- mockFullNode := &provider_mocks.Provider{}
- mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
- _, err = c.TrustedLightBlock(1)
- require.NoError(t, err)
-
- err = c.Cleanup()
- require.NoError(t, err)
-
- // Check no light blocks exist after Cleanup.
- l, err := c.TrustedLightBlock(1)
- assert.Error(t, err)
- assert.Nil(t, l)
- mockFullNode.AssertExpectations(t)
- }
-
- // trustedHeader.Height == options.Height
- func TestClientRestoresTrustedHeaderAfterStartup(t *testing.T) {
- // 1. options.Hash == trustedHeader.Hash
- t.Run("hashes should match", func(t *testing.T) {
- mockNode := &provider_mocks.Provider{}
- trustedStore := dbs.New(dbm.NewMemDB())
- err := trustedStore.SaveLightBlock(l1)
- require.NoError(t, err)
-
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockNode,
- []provider.Provider{mockNode},
- trustedStore,
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- l, err := c.TrustedLightBlock(1)
- assert.NoError(t, err)
- assert.NotNil(t, l)
- assert.Equal(t, l.Hash(), h1.Hash())
- assert.Equal(t, l.ValidatorSet.Hash(), h1.ValidatorsHash.Bytes())
- mockNode.AssertExpectations(t)
- })
-
- // 2. options.Hash != trustedHeader.Hash
- t.Run("hashes should not match", func(t *testing.T) {
- trustedStore := dbs.New(dbm.NewMemDB())
- err := trustedStore.SaveLightBlock(l1)
- require.NoError(t, err)
-
- // header1 != h1
- header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
- hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys))
- mockNode := &provider_mocks.Provider{}
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: header1.Hash(),
- },
- mockNode,
- []provider.Provider{mockNode},
- trustedStore,
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- l, err := c.TrustedLightBlock(1)
- assert.NoError(t, err)
- if assert.NotNil(t, l) {
- // client take the trusted store and ignores the trusted options
- assert.Equal(t, l.Hash(), l1.Hash())
- assert.NoError(t, l.ValidateBasic(chainID))
- }
- mockNode.AssertExpectations(t)
- })
- }
-
- func TestClient_Update(t *testing.T) {
- mockFullNode := &provider_mocks.Provider{}
- mockFullNode.On("LightBlock", mock.Anything, int64(0)).Return(l3, nil)
- mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
- mockFullNode.On("LightBlock", mock.Anything, int64(3)).Return(l3, nil)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- // should result in downloading & verifying header #3
- l, err := c.Update(ctx, bTime.Add(2*time.Hour))
- assert.NoError(t, err)
- if assert.NotNil(t, l) {
- assert.EqualValues(t, 3, l.Height)
- assert.NoError(t, l.ValidateBasic(chainID))
- }
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClient_Concurrency(t *testing.T) {
- mockFullNode := &provider_mocks.Provider{}
- mockFullNode.On("LightBlock", mock.Anything, int64(2)).Return(l2, nil)
- mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
- require.NoError(t, err)
-
- var wg sync.WaitGroup
- for i := 0; i < 100; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
-
- // NOTE: Cleanup, Stop, VerifyLightBlockAtHeight and Verify are not supposed
- // to be concurrently safe.
-
- assert.Equal(t, chainID, c.ChainID())
-
- _, err := c.LastTrustedHeight()
- assert.NoError(t, err)
-
- _, err = c.FirstTrustedHeight()
- assert.NoError(t, err)
-
- l, err := c.TrustedLightBlock(1)
- assert.NoError(t, err)
- assert.NotNil(t, l)
- }()
- }
-
- wg.Wait()
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClient_AddProviders(t *testing.T) {
- mockFullNode := mockNodeFromHeadersAndVals(map[int64]*types.SignedHeader{
- 1: h1,
- 2: h2,
- }, valSet)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- closeCh := make(chan struct{})
- go func() {
- // run verification concurrently to make sure it doesn't dead lock
- _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
- require.NoError(t, err)
- close(closeCh)
- }()
-
- // NOTE: the light client doesn't check uniqueness of providers
- c.AddProvider(mockFullNode)
- require.Len(t, c.Witnesses(), 2)
- select {
- case <-closeCh:
- case <-time.After(5 * time.Second):
- t.Fatal("concurent light block verification failed to finish in 5s")
- }
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) {
- mockFullNode := &provider_mocks.Provider{}
- mockFullNode.On("LightBlock", mock.Anything, mock.Anything).Return(l1, nil)
-
- mockDeadNode := &provider_mocks.Provider{}
- mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrNoResponse)
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockDeadNode,
- []provider.Provider{mockFullNode, mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
-
- require.NoError(t, err)
- _, err = c.Update(ctx, bTime.Add(2*time.Hour))
- require.NoError(t, err)
-
- // the primary should no longer be the deadNode
- assert.NotEqual(t, c.Primary(), mockDeadNode)
-
- // we should still have the dead node as a witness because it
- // hasn't repeatedly been unresponsive yet
- assert.Equal(t, 2, len(c.Witnesses()))
- mockDeadNode.AssertExpectations(t)
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClient_BackwardsVerification(t *testing.T) {
- {
- headers, vals, _ := genLightBlocksWithKeys(chainID, 9, 3, 0, bTime)
- delete(headers, 1)
- delete(headers, 2)
- delete(vals, 1)
- delete(vals, 2)
- mockLargeFullNode := mockNodeFromHeadersAndVals(headers, vals)
- trustHeader, _ := mockLargeFullNode.LightBlock(ctx, 6)
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Minute,
- Height: trustHeader.Height,
- Hash: trustHeader.Hash(),
- },
- mockLargeFullNode,
- []provider.Provider{mockLargeFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- // 1) verify before the trusted header using backwards => expect no error
- h, err := c.VerifyLightBlockAtHeight(ctx, 5, bTime.Add(6*time.Minute))
- require.NoError(t, err)
- if assert.NotNil(t, h) {
- assert.EqualValues(t, 5, h.Height)
- }
-
- // 2) untrusted header is expired but trusted header is not => expect no error
- h, err = c.VerifyLightBlockAtHeight(ctx, 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.VerifyLightBlockAtHeight(ctx, 5, bTime.Add(6*time.Minute))
- assert.NoError(t, err)
- assert.NotNil(t, h)
-
- // 4a) First verify latest header
- _, err = c.VerifyLightBlockAtHeight(ctx, 9, bTime.Add(9*time.Minute))
- require.NoError(t, err)
-
- // 4b) Verify backwards using bisection => expect no error
- _, err = c.VerifyLightBlockAtHeight(ctx, 7, bTime.Add(9*time.Minute))
- assert.NoError(t, err)
- // shouldn't have verified this header in the process
- _, err = c.TrustedLightBlock(8)
- assert.Error(t, err)
-
- // 5) Try bisection method, but closest header (at 7) has expired
- // so expect error
- _, err = c.VerifyLightBlockAtHeight(ctx, 8, bTime.Add(12*time.Minute))
- assert.Error(t, err)
- mockLargeFullNode.AssertExpectations(t)
-
- }
- {
- // 8) provides incorrect hash
- headers := map[int64]*types.SignedHeader{
- 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
- hash("app_hash2"), hash("cons_hash23"), hash("results_hash30"), 0, len(keys)),
- 3: h3,
- }
- vals := valSet
- mockNode := mockNodeFromHeadersAndVals(headers, vals)
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 1 * time.Hour,
- Height: 3,
- Hash: h3.Hash(),
- },
- mockNode,
- []provider.Provider{mockNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour).Add(1*time.Second))
- assert.Error(t, err)
- mockNode.AssertExpectations(t)
- }
- }
-
- func TestClient_NewClientFromTrustedStore(t *testing.T) {
- // 1) Initiate DB and fill with a "trusted" header
- db := dbs.New(dbm.NewMemDB())
- err := db.SaveLightBlock(l1)
- require.NoError(t, err)
- mockNode := &provider_mocks.Provider{}
-
- c, err := light.NewClientFromTrustedStore(
- chainID,
- trustPeriod,
- mockNode,
- []provider.Provider{mockNode},
- db,
- )
- require.NoError(t, err)
-
- // 2) Check light block exists
- h, err := c.TrustedLightBlock(1)
- assert.NoError(t, err)
- assert.EqualValues(t, l1.Height, h.Height)
- mockNode.AssertExpectations(t)
- }
-
- func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) {
- // different headers hash then primary plus less than 1/3 signed (no fork)
- headers1 := map[int64]*types.SignedHeader{
- 1: h1,
- 2: keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
- hash("app_hash2"), hash("cons_hash"), hash("results_hash"),
- len(keys), len(keys), types.BlockID{Hash: h1.Hash()}),
- }
- vals1 := map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- }
- mockBadNode1 := mockNodeFromHeadersAndVals(headers1, vals1)
- mockBadNode1.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
-
- // header is empty
- headers2 := map[int64]*types.SignedHeader{
- 1: h1,
- 2: h2,
- }
- vals2 := map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- }
- mockBadNode2 := mockNodeFromHeadersAndVals(headers2, vals2)
- mockBadNode2.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
-
- mockFullNode := mockNodeFromHeadersAndVals(headerSet, valSet)
-
- lb1, _ := mockBadNode1.LightBlock(ctx, 2)
- require.NotEqual(t, lb1.Hash(), l1.Hash())
-
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockBadNode1, mockBadNode2},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- // witness should have behaved properly -> no error
- require.NoError(t, err)
- assert.EqualValues(t, 2, len(c.Witnesses()))
-
- // witness behaves incorrectly -> removed from list, no error
- l, err := c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
- assert.NoError(t, err)
- assert.EqualValues(t, 1, len(c.Witnesses()))
- // light block should still be verified
- assert.EqualValues(t, 2, l.Height)
-
- // remaining witnesses don't have light block -> error
- _, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
- if assert.Error(t, err) {
- assert.Equal(t, light.ErrFailedHeaderCrossReferencing, err)
- }
- // witness does not have a light block -> left in the list
- assert.EqualValues(t, 1, len(c.Witnesses()))
- mockBadNode1.AssertExpectations(t)
- mockBadNode2.AssertExpectations(t)
- }
-
- func TestClient_TrustedValidatorSet(t *testing.T) {
- differentVals, _ := factory.RandValidatorSet(10, 100)
- mockBadValSetNode := mockNodeFromHeadersAndVals(
- map[int64]*types.SignedHeader{
- 1: h1,
- // 3/3 signed, but validator set at height 2 below is invalid -> witness
- // should be removed.
- 2: keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
- hash("app_hash2"), hash("cons_hash"), hash("results_hash"),
- 0, len(keys), types.BlockID{Hash: h1.Hash()}),
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: differentVals,
- })
- mockFullNode := mockNodeFromHeadersAndVals(
- map[int64]*types.SignedHeader{
- 1: h1,
- 2: h2,
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 2: vals,
- })
-
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockBadValSetNode, mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
- assert.Equal(t, 2, len(c.Witnesses()))
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour).Add(1*time.Second))
- assert.NoError(t, err)
- assert.Equal(t, 1, len(c.Witnesses()))
- mockBadValSetNode.AssertExpectations(t)
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClientPrunesHeadersAndValidatorSets(t *testing.T) {
- mockFullNode := mockNodeFromHeadersAndVals(
- map[int64]*types.SignedHeader{
- 1: h1,
- 3: h3,
- 0: h3,
- },
- map[int64]*types.ValidatorSet{
- 1: vals,
- 3: vals,
- 0: vals,
- })
-
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockFullNode,
- []provider.Provider{mockFullNode},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- light.PruningSize(1),
- )
- require.NoError(t, err)
- _, err = c.TrustedLightBlock(1)
- require.NoError(t, err)
-
- h, err := c.Update(ctx, bTime.Add(2*time.Hour))
- require.NoError(t, err)
- require.Equal(t, int64(3), h.Height)
-
- _, err = c.TrustedLightBlock(1)
- assert.Error(t, err)
- mockFullNode.AssertExpectations(t)
- }
-
- func TestClientEnsureValidHeadersAndValSets(t *testing.T) {
- emptyValSet := &types.ValidatorSet{
- Validators: nil,
- Proposer: nil,
- }
-
- testCases := []struct {
- headers map[int64]*types.SignedHeader
- vals map[int64]*types.ValidatorSet
-
- errorToThrow error
- errorHeight int64
-
- err bool
- }{
- {
- headers: map[int64]*types.SignedHeader{
- 1: h1,
- 3: h3,
- },
- vals: map[int64]*types.ValidatorSet{
- 1: vals,
- 3: vals,
- },
- err: false,
- },
- {
- headers: map[int64]*types.SignedHeader{
- 1: h1,
- },
- vals: map[int64]*types.ValidatorSet{
- 1: vals,
- },
- errorToThrow: provider.ErrBadLightBlock{Reason: errors.New("nil header or vals")},
- errorHeight: 3,
- err: true,
- },
- {
- headers: map[int64]*types.SignedHeader{
- 1: h1,
- },
- errorToThrow: provider.ErrBadLightBlock{Reason: errors.New("nil header or vals")},
- errorHeight: 3,
- vals: valSet,
- err: true,
- },
- {
- headers: map[int64]*types.SignedHeader{
- 1: h1,
- 3: h3,
- },
- vals: map[int64]*types.ValidatorSet{
- 1: vals,
- 3: emptyValSet,
- },
- err: true,
- },
- }
-
- for i, tc := range testCases {
- testCase := tc
- t.Run(fmt.Sprintf("case: %d", i), func(t *testing.T) {
- mockBadNode := mockNodeFromHeadersAndVals(testCase.headers, testCase.vals)
- if testCase.errorToThrow != nil {
- mockBadNode.On("LightBlock", mock.Anything, testCase.errorHeight).Return(nil, testCase.errorToThrow)
- }
-
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockBadNode,
- []provider.Provider{mockBadNode, mockBadNode},
- dbs.New(dbm.NewMemDB()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
- if testCase.err {
- assert.Error(t, err)
- } else {
- assert.NoError(t, err)
- }
- mockBadNode.AssertExpectations(t)
- })
- }
-
- }
-
- func TestClientHandlesContexts(t *testing.T) {
- mockNode := &provider_mocks.Provider{}
- mockNode.On("LightBlock",
- mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == nil }),
- int64(1)).Return(l1, nil)
- mockNode.On("LightBlock",
- mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == context.DeadlineExceeded }),
- mock.Anything).Return(nil, context.DeadlineExceeded)
-
- mockNode.On("LightBlock",
- mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == context.Canceled }),
- mock.Anything).Return(nil, context.Canceled)
-
- // instantiate the light client with a timeout
- ctxTimeOut, cancel := context.WithTimeout(ctx, 1*time.Nanosecond)
- defer cancel()
- _, err := light.NewClient(
- ctxTimeOut,
- chainID,
- trustOptions,
- mockNode,
- []provider.Provider{mockNode, mockNode},
- dbs.New(dbm.NewMemDB()),
- )
- require.Error(t, ctxTimeOut.Err())
- require.Error(t, err)
- require.True(t, errors.Is(err, context.DeadlineExceeded))
-
- // instantiate the client for real
- c, err := light.NewClient(
- ctx,
- chainID,
- trustOptions,
- mockNode,
- []provider.Provider{mockNode, mockNode},
- dbs.New(dbm.NewMemDB()),
- )
- require.NoError(t, err)
-
- // verify a block with a timeout
- ctxTimeOutBlock, cancel := context.WithTimeout(ctx, 1*time.Nanosecond)
- defer cancel()
- _, err = c.VerifyLightBlockAtHeight(ctxTimeOutBlock, 100, bTime.Add(100*time.Minute))
- require.Error(t, ctxTimeOutBlock.Err())
- require.Error(t, err)
- require.True(t, errors.Is(err, context.DeadlineExceeded))
-
- // verify a block with a cancel
- ctxCancel, cancel := context.WithCancel(ctx)
- cancel()
- _, err = c.VerifyLightBlockAtHeight(ctxCancel, 100, bTime.Add(100*time.Minute))
- require.Error(t, ctxCancel.Err())
- require.Error(t, err)
- require.True(t, errors.Is(err, context.Canceled))
- mockNode.AssertExpectations(t)
-
- }
|