github.com/vipernet-xyz/tm@v0.34.24/light/detector_test.go (about)

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