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