github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/light/detector_test.go (about)

     1  package light_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"testing"
     7  	"time"
     8  
     9  	provider_mocks "github.com/ari-anchor/sei-tendermint/light/provider/mocks"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	dbm "github.com/tendermint/tm-db"
    16  
    17  	"github.com/ari-anchor/sei-tendermint/libs/log"
    18  	"github.com/ari-anchor/sei-tendermint/light"
    19  	"github.com/ari-anchor/sei-tendermint/light/provider"
    20  	dbs "github.com/ari-anchor/sei-tendermint/light/store/db"
    21  	"github.com/ari-anchor/sei-tendermint/types"
    22  )
    23  
    24  func TestLightClientAttackEvidence_Lunatic(t *testing.T) {
    25  	logger := log.NewNopLogger()
    26  
    27  	// primary performs a lunatic attack
    28  	var (
    29  		latestHeight      = int64(3)
    30  		valSize           = 5
    31  		divergenceHeight  = int64(2)
    32  		primaryHeaders    = make(map[int64]*types.SignedHeader, latestHeight)
    33  		primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
    34  	)
    35  
    36  	ctx, cancel := context.WithCancel(context.Background())
    37  	defer cancel()
    38  
    39  	witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t, latestHeight, valSize, 2, bTime)
    40  
    41  	forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
    42  	forgedVals := forgedKeys.ToValidators(2, 0)
    43  
    44  	for height := int64(1); height <= latestHeight; height++ {
    45  		if height < divergenceHeight {
    46  			primaryHeaders[height] = witnessHeaders[height]
    47  			primaryValidators[height] = witnessValidators[height]
    48  			continue
    49  		}
    50  		primaryHeaders[height] = forgedKeys.GenSignedHeader(t, chainID, height, bTime.Add(time.Duration(height)*time.Minute),
    51  			nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys))
    52  		primaryValidators[height] = forgedVals
    53  	}
    54  
    55  	// never called, delete it to make mockery asserts pass
    56  	delete(witnessHeaders, 2)
    57  	delete(primaryHeaders, 2)
    58  
    59  	mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
    60  	mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
    61  	mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
    62  		evAgainstPrimary := &types.LightClientAttackEvidence{
    63  			// after the divergence height the valset doesn't change so we expect the evidence to be for the latest height
    64  			ConflictingBlock: &types.LightBlock{
    65  				SignedHeader: primaryHeaders[latestHeight],
    66  				ValidatorSet: primaryValidators[latestHeight],
    67  			},
    68  			CommonHeight: 1,
    69  		}
    70  		return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
    71  	})).Return(nil)
    72  
    73  	mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
    74  		evAgainstWitness := &types.LightClientAttackEvidence{
    75  			// when forming evidence against witness we learn that the canonical chain continued to change validator sets
    76  			// hence the conflicting block is at 7
    77  			ConflictingBlock: &types.LightBlock{
    78  				SignedHeader: witnessHeaders[divergenceHeight+1],
    79  				ValidatorSet: witnessValidators[divergenceHeight+1],
    80  			},
    81  			CommonHeight: divergenceHeight - 1,
    82  		}
    83  		return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash())
    84  	})).Return(nil)
    85  
    86  	c, err := light.NewClient(
    87  		ctx,
    88  		chainID,
    89  		light.TrustOptions{
    90  			Period: 4 * time.Hour,
    91  			Height: 1,
    92  			Hash:   primaryHeaders[1].Hash(),
    93  		},
    94  		mockPrimary,
    95  		[]provider.Provider{mockWitness},
    96  		dbs.New(dbm.NewMemDB()),
    97  		light.Logger(logger),
    98  	)
    99  	require.NoError(t, err)
   100  
   101  	// Check verification returns an error.
   102  	_, err = c.VerifyLightBlockAtHeight(ctx, latestHeight, bTime.Add(1*time.Hour))
   103  	if assert.Error(t, err) {
   104  		assert.Equal(t, light.ErrLightClientAttack, err)
   105  	}
   106  
   107  	mockWitness.AssertExpectations(t)
   108  	mockPrimary.AssertExpectations(t)
   109  }
   110  
   111  func TestLightClientAttackEvidence_Equivocation(t *testing.T) {
   112  	cases := []struct {
   113  		name                      string
   114  		lightOption               light.Option
   115  		unusedWitnessBlockHeights []int64
   116  		unusedPrimaryBlockHeights []int64
   117  		latestHeight              int64
   118  		divergenceHeight          int64
   119  	}{
   120  		{
   121  			name:                      "sequential",
   122  			lightOption:               light.SequentialVerification(),
   123  			unusedWitnessBlockHeights: []int64{4, 6},
   124  			latestHeight:              int64(5),
   125  			divergenceHeight:          int64(3),
   126  		},
   127  		{
   128  			name:                      "skipping",
   129  			lightOption:               light.SkippingVerification(light.DefaultTrustLevel),
   130  			unusedWitnessBlockHeights: []int64{2, 4, 6},
   131  			unusedPrimaryBlockHeights: []int64{2, 4, 6},
   132  			latestHeight:              int64(5),
   133  			divergenceHeight:          int64(3),
   134  		},
   135  	}
   136  
   137  	bctx, bcancel := context.WithCancel(context.Background())
   138  	defer bcancel()
   139  
   140  	for _, tc := range cases {
   141  		testCase := tc
   142  		t.Run(testCase.name, func(t *testing.T) {
   143  			ctx, cancel := context.WithCancel(bctx)
   144  			defer cancel()
   145  
   146  			logger := log.NewNopLogger()
   147  
   148  			// primary performs an equivocation attack
   149  			var (
   150  				valSize        = 5
   151  				primaryHeaders = make(map[int64]*types.SignedHeader, testCase.latestHeight)
   152  				// validators don't change in this network (however we still use a map just for convenience)
   153  				primaryValidators = make(map[int64]*types.ValidatorSet, testCase.latestHeight)
   154  			)
   155  			witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t,
   156  				testCase.latestHeight+1, valSize, 2, bTime)
   157  			for height := int64(1); height <= testCase.latestHeight; height++ {
   158  				if height < testCase.divergenceHeight {
   159  					primaryHeaders[height] = witnessHeaders[height]
   160  					primaryValidators[height] = witnessValidators[height]
   161  					continue
   162  				}
   163  				// we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for
   164  				// a different block (which we do by adding txs)
   165  				primaryHeaders[height] = chainKeys[height].GenSignedHeader(t, chainID, height,
   166  					bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")},
   167  					witnessValidators[height], witnessValidators[height+1], hash("app_hash"),
   168  					hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1)
   169  				primaryValidators[height] = witnessValidators[height]
   170  			}
   171  
   172  			for _, height := range testCase.unusedWitnessBlockHeights {
   173  				delete(witnessHeaders, height)
   174  			}
   175  			mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
   176  
   177  			for _, height := range testCase.unusedPrimaryBlockHeights {
   178  				delete(primaryHeaders, height)
   179  			}
   180  			mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
   181  
   182  			// Check evidence was sent to both full nodes.
   183  			// Common height should be set to the height of the divergent header in the instance
   184  			// of an equivocation attack and the validator sets are the same as what the witness has
   185  			mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
   186  				evAgainstPrimary := &types.LightClientAttackEvidence{
   187  					ConflictingBlock: &types.LightBlock{
   188  						SignedHeader: primaryHeaders[testCase.divergenceHeight],
   189  						ValidatorSet: primaryValidators[testCase.divergenceHeight],
   190  					},
   191  					CommonHeight: testCase.divergenceHeight,
   192  				}
   193  				return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
   194  			})).Return(nil)
   195  			mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
   196  				evAgainstWitness := &types.LightClientAttackEvidence{
   197  					ConflictingBlock: &types.LightBlock{
   198  						SignedHeader: witnessHeaders[testCase.divergenceHeight],
   199  						ValidatorSet: witnessValidators[testCase.divergenceHeight],
   200  					},
   201  					CommonHeight: testCase.divergenceHeight,
   202  				}
   203  				return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash())
   204  			})).Return(nil)
   205  
   206  			c, err := light.NewClient(
   207  				ctx,
   208  				chainID,
   209  				light.TrustOptions{
   210  					Period: 4 * time.Hour,
   211  					Height: 1,
   212  					Hash:   primaryHeaders[1].Hash(),
   213  				},
   214  				mockPrimary,
   215  				[]provider.Provider{mockWitness},
   216  				dbs.New(dbm.NewMemDB()),
   217  				light.Logger(logger),
   218  				testCase.lightOption,
   219  			)
   220  			require.NoError(t, err)
   221  
   222  			// Check verification returns an error.
   223  			_, err = c.VerifyLightBlockAtHeight(ctx, testCase.latestHeight, bTime.Add(300*time.Second))
   224  			if assert.Error(t, err) {
   225  				assert.Equal(t, light.ErrLightClientAttack, err)
   226  			}
   227  
   228  			mockWitness.AssertExpectations(t)
   229  			mockPrimary.AssertExpectations(t)
   230  		})
   231  	}
   232  }
   233  
   234  func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) {
   235  	// primary performs a lunatic attack but changes the time of the header to
   236  	// something in the future relative to the blockchain
   237  	var (
   238  		latestHeight      = int64(10)
   239  		valSize           = 5
   240  		forgedHeight      = int64(12)
   241  		proofHeight       = int64(11)
   242  		primaryHeaders    = make(map[int64]*types.SignedHeader, forgedHeight)
   243  		primaryValidators = make(map[int64]*types.ValidatorSet, forgedHeight)
   244  	)
   245  
   246  	ctx, cancel := context.WithCancel(context.Background())
   247  	defer cancel()
   248  	logger := log.NewNopLogger()
   249  
   250  	witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t, latestHeight, valSize, 2, bTime)
   251  	for _, unusedHeader := range []int64{3, 5, 6, 8} {
   252  		delete(witnessHeaders, unusedHeader)
   253  	}
   254  
   255  	// primary has the exact same headers except it forges one extra header in the future using keys from 2/5ths of
   256  	// the validators
   257  	for h := range witnessHeaders {
   258  		primaryHeaders[h] = witnessHeaders[h]
   259  		primaryValidators[h] = witnessValidators[h]
   260  	}
   261  	for _, unusedHeader := range []int64{3, 5, 6, 8} {
   262  		delete(primaryHeaders, unusedHeader)
   263  	}
   264  	forgedKeys := chainKeys[latestHeight].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
   265  	primaryValidators[forgedHeight] = forgedKeys.ToValidators(2, 0)
   266  	primaryHeaders[forgedHeight] = forgedKeys.GenSignedHeader(t,
   267  		chainID,
   268  		forgedHeight,
   269  		bTime.Add(time.Duration(latestHeight+1)*time.Minute), // 11 mins
   270  		nil,
   271  		primaryValidators[forgedHeight],
   272  		primaryValidators[forgedHeight],
   273  		hash("app_hash"),
   274  		hash("cons_hash"),
   275  		hash("results_hash"),
   276  		0, len(forgedKeys),
   277  	)
   278  	mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
   279  	lastBlock, _ := mockPrimary.LightBlock(ctx, forgedHeight)
   280  	mockPrimary.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
   281  	mockPrimary.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
   282  
   283  	mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
   284  	lastBlock, _ = mockWitness.LightBlock(ctx, latestHeight)
   285  	mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil).Once()
   286  	mockWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh)
   287  
   288  	mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
   289  		// Check evidence was sent to the witness against the full node
   290  		evAgainstPrimary := &types.LightClientAttackEvidence{
   291  			ConflictingBlock: &types.LightBlock{
   292  				SignedHeader: primaryHeaders[forgedHeight],
   293  				ValidatorSet: primaryValidators[forgedHeight],
   294  			},
   295  			CommonHeight: latestHeight,
   296  		}
   297  		return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
   298  	})).Return(nil).Twice()
   299  
   300  	// In order to perform the attack, the primary needs at least one accomplice as a witness to also
   301  	// send the forged block
   302  	accomplice := mockPrimary
   303  
   304  	c, err := light.NewClient(
   305  		ctx,
   306  		chainID,
   307  		light.TrustOptions{
   308  			Period: 4 * time.Hour,
   309  			Height: 1,
   310  			Hash:   primaryHeaders[1].Hash(),
   311  		},
   312  		mockPrimary,
   313  		[]provider.Provider{mockWitness, accomplice},
   314  		dbs.New(dbm.NewMemDB()),
   315  		light.Logger(logger),
   316  		light.MaxClockDrift(1*time.Second),
   317  		light.MaxBlockLag(1*time.Second),
   318  	)
   319  	require.NoError(t, err)
   320  
   321  	// two seconds later, the supporting withness should receive the header that can be used
   322  	// to prove that there was an attack
   323  	vals := chainKeys[latestHeight].ToValidators(2, 0)
   324  	newLb := &types.LightBlock{
   325  		SignedHeader: chainKeys[latestHeight].GenSignedHeader(t,
   326  			chainID,
   327  			proofHeight,
   328  			bTime.Add(time.Duration(proofHeight+1)*time.Minute), // 12 mins
   329  			nil,
   330  			vals,
   331  			vals,
   332  			hash("app_hash"),
   333  			hash("cons_hash"),
   334  			hash("results_hash"),
   335  			0, len(chainKeys),
   336  		),
   337  		ValidatorSet: vals,
   338  	}
   339  	go func() {
   340  		time.Sleep(2 * time.Second)
   341  		mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(newLb, nil)
   342  	}()
   343  
   344  	// Now assert that verification returns an error. We craft the light clients time to be a little ahead of the chain
   345  	// to allow a window for the attack to manifest itself.
   346  	_, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
   347  	if assert.Error(t, err) {
   348  		assert.Equal(t, light.ErrLightClientAttack, err)
   349  	}
   350  
   351  	// We attempt the same call but now the supporting witness has a block which should
   352  	// immediately conflict in time with the primary
   353  	_, err = c.VerifyLightBlockAtHeight(ctx, forgedHeight, bTime.Add(time.Duration(forgedHeight)*time.Minute))
   354  	if assert.Error(t, err) {
   355  		assert.Equal(t, light.ErrLightClientAttack, err)
   356  	}
   357  
   358  	// Lastly we test the unfortunate case where the light clients supporting witness doesn't update
   359  	// in enough time
   360  	mockLaggingWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
   361  	mockLaggingWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh)
   362  	lastBlock, _ = mockLaggingWitness.LightBlock(ctx, latestHeight)
   363  	mockLaggingWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
   364  	c, err = light.NewClient(
   365  		ctx,
   366  		chainID,
   367  		light.TrustOptions{
   368  			Period: 4 * time.Hour,
   369  			Height: 1,
   370  			Hash:   primaryHeaders[1].Hash(),
   371  		},
   372  		mockPrimary,
   373  		[]provider.Provider{mockLaggingWitness, accomplice},
   374  		dbs.New(dbm.NewMemDB()),
   375  		light.Logger(logger),
   376  		light.MaxClockDrift(1*time.Second),
   377  		light.MaxBlockLag(1*time.Second),
   378  	)
   379  	require.NoError(t, err)
   380  
   381  	_, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
   382  	assert.NoError(t, err)
   383  	mockPrimary.AssertExpectations(t)
   384  }
   385  
   386  // 1. Different nodes therefore a divergent header is produced.
   387  // => light client returns an error upon creation because primary and witness
   388  // have a different view.
   389  func TestClientDivergentTraces1(t *testing.T) {
   390  	ctx, cancel := context.WithCancel(context.Background())
   391  	defer cancel()
   392  
   393  	headers, vals, _ := genLightBlocksWithKeys(t, 1, 5, 2, bTime)
   394  	mockPrimary := mockNodeFromHeadersAndVals(headers, vals)
   395  
   396  	firstBlock, err := mockPrimary.LightBlock(ctx, 1)
   397  	require.NoError(t, err)
   398  	headers, vals, _ = genLightBlocksWithKeys(t, 1, 5, 2, bTime)
   399  	mockWitness := mockNodeFromHeadersAndVals(headers, vals)
   400  
   401  	logger := log.NewNopLogger()
   402  
   403  	_, err = light.NewClient(
   404  		ctx,
   405  		chainID,
   406  		light.TrustOptions{
   407  			Height: 1,
   408  			Hash:   firstBlock.Hash(),
   409  			Period: 4 * time.Hour,
   410  		},
   411  		mockPrimary,
   412  		[]provider.Provider{mockWitness},
   413  		dbs.New(dbm.NewMemDB()),
   414  		light.Logger(logger),
   415  	)
   416  	require.Error(t, err)
   417  	assert.Contains(t, err.Error(), "does not match primary")
   418  	mockWitness.AssertExpectations(t)
   419  	mockPrimary.AssertExpectations(t)
   420  }
   421  
   422  // 2. Two out of three nodes don't respond but the third has a header that matches
   423  // => verification should be successful and all the witnesses should remain
   424  func TestClientDivergentTraces2(t *testing.T) {
   425  	ctx, cancel := context.WithCancel(context.Background())
   426  	defer cancel()
   427  	logger := log.NewNopLogger()
   428  
   429  	headers, vals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime)
   430  	mockPrimaryNode := mockNodeFromHeadersAndVals(headers, vals)
   431  	mockDeadNode := &provider_mocks.Provider{}
   432  	mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrNoResponse)
   433  	firstBlock, err := mockPrimaryNode.LightBlock(ctx, 1)
   434  	require.NoError(t, err)
   435  	c, err := light.NewClient(
   436  		ctx,
   437  		chainID,
   438  		light.TrustOptions{
   439  			Height: 1,
   440  			Hash:   firstBlock.Hash(),
   441  			Period: 4 * time.Hour,
   442  		},
   443  		mockPrimaryNode,
   444  		[]provider.Provider{mockDeadNode, mockDeadNode, mockPrimaryNode},
   445  		dbs.New(dbm.NewMemDB()),
   446  		light.Logger(logger),
   447  	)
   448  	require.NoError(t, err)
   449  
   450  	_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
   451  	assert.NoError(t, err)
   452  	assert.Equal(t, 3, len(c.Witnesses()))
   453  	mockDeadNode.AssertExpectations(t)
   454  	mockPrimaryNode.AssertExpectations(t)
   455  }
   456  
   457  // 3. witness has the same first header, but different second header
   458  // => creation should succeed, but the verification should fail
   459  // nolint: dupl
   460  func TestClientDivergentTraces3(t *testing.T) {
   461  	logger := log.NewNopLogger()
   462  
   463  	//
   464  	primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime)
   465  	mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
   466  
   467  	ctx, cancel := context.WithCancel(context.Background())
   468  	defer cancel()
   469  
   470  	firstBlock, err := mockPrimary.LightBlock(ctx, 1)
   471  	require.NoError(t, err)
   472  
   473  	mockHeaders, mockVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime)
   474  	mockHeaders[1] = primaryHeaders[1]
   475  	mockVals[1] = primaryVals[1]
   476  	mockWitness := mockNodeFromHeadersAndVals(mockHeaders, mockVals)
   477  
   478  	c, err := light.NewClient(
   479  		ctx,
   480  		chainID,
   481  		light.TrustOptions{
   482  			Height: 1,
   483  			Hash:   firstBlock.Hash(),
   484  			Period: 4 * time.Hour,
   485  		},
   486  		mockPrimary,
   487  		[]provider.Provider{mockWitness},
   488  		dbs.New(dbm.NewMemDB()),
   489  		light.Logger(logger),
   490  	)
   491  	require.NoError(t, err)
   492  
   493  	_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
   494  	assert.Error(t, err)
   495  	assert.Equal(t, 1, len(c.Witnesses()))
   496  	mockWitness.AssertExpectations(t)
   497  	mockPrimary.AssertExpectations(t)
   498  }
   499  
   500  // 4. Witness has a divergent header but can not produce a valid trace to back it up.
   501  // It should be ignored
   502  // nolint: dupl
   503  func TestClientDivergentTraces4(t *testing.T) {
   504  	logger := log.NewNopLogger()
   505  
   506  	//
   507  	primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime)
   508  	mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
   509  
   510  	ctx, cancel := context.WithCancel(context.Background())
   511  	defer cancel()
   512  
   513  	firstBlock, err := mockPrimary.LightBlock(ctx, 1)
   514  	require.NoError(t, err)
   515  
   516  	witnessHeaders, witnessVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime)
   517  	primaryHeaders[2] = witnessHeaders[2]
   518  	primaryVals[2] = witnessVals[2]
   519  	mockWitness := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
   520  
   521  	c, err := light.NewClient(
   522  		ctx,
   523  		chainID,
   524  		light.TrustOptions{
   525  			Height: 1,
   526  			Hash:   firstBlock.Hash(),
   527  			Period: 4 * time.Hour,
   528  		},
   529  		mockPrimary,
   530  		[]provider.Provider{mockWitness},
   531  		dbs.New(dbm.NewMemDB()),
   532  		light.Logger(logger),
   533  	)
   534  	require.NoError(t, err)
   535  
   536  	_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
   537  	assert.Error(t, err)
   538  	assert.Equal(t, 1, len(c.Witnesses()))
   539  	mockWitness.AssertExpectations(t)
   540  	mockPrimary.AssertExpectations(t)
   541  }