- package light_test
-
- import (
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- dbm "github.com/tendermint/tm-db"
-
- "github.com/tendermint/tendermint/libs/log"
- "github.com/tendermint/tendermint/light"
- "github.com/tendermint/tendermint/light/provider"
- mockp "github.com/tendermint/tendermint/light/provider/mock"
- dbs "github.com/tendermint/tendermint/light/store/db"
- "github.com/tendermint/tendermint/types"
- )
-
- func TestLightClientAttackEvidence_Lunatic(t *testing.T) {
- // primary performs a lunatic attack
- var (
- latestHeight = int64(10)
- valSize = 5
- divergenceHeight = int64(6)
- primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight)
- primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
- )
-
- witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight, valSize, 2, bTime)
- witness := mockp.New(chainID, witnessHeaders, witnessValidators)
- forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
- forgedVals := forgedKeys.ToValidators(2, 0)
-
- for height := int64(1); height <= latestHeight; height++ {
- if height < divergenceHeight {
- primaryHeaders[height] = witnessHeaders[height]
- primaryValidators[height] = witnessValidators[height]
- continue
- }
- primaryHeaders[height] = forgedKeys.GenSignedHeader(chainID, height, bTime.Add(time.Duration(height)*time.Minute),
- nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys))
- primaryValidators[height] = forgedVals
- }
- primary := mockp.New(chainID, primaryHeaders, primaryValidators)
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: primaryHeaders[1].Hash(),
- },
- primary,
- []provider.Provider{witness},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- // Check verification returns an error.
- _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
- if assert.Error(t, err) {
- assert.Equal(t, light.ErrLightClientAttack, err)
- }
-
- // Check evidence was sent to both full nodes.
- evAgainstPrimary := &types.LightClientAttackEvidence{
- // after the divergence height the valset doesn't change so we expect the evidence to be for height 10
- ConflictingBlock: &types.LightBlock{
- SignedHeader: primaryHeaders[10],
- ValidatorSet: primaryValidators[10],
- },
- CommonHeight: 4,
- }
- assert.True(t, witness.HasEvidence(evAgainstPrimary))
-
- evAgainstWitness := &types.LightClientAttackEvidence{
- // when forming evidence against witness we learn that the canonical chain continued to change validator sets
- // hence the conflicting block is at 7
- ConflictingBlock: &types.LightBlock{
- SignedHeader: witnessHeaders[7],
- ValidatorSet: witnessValidators[7],
- },
- CommonHeight: 4,
- }
- assert.True(t, primary.HasEvidence(evAgainstWitness))
- }
-
- func TestLightClientAttackEvidence_Equivocation(t *testing.T) {
- verificationOptions := map[string]light.Option{
- "sequential": light.SequentialVerification(),
- "skipping": light.SkippingVerification(light.DefaultTrustLevel),
- }
-
- for s, verificationOption := range verificationOptions {
- t.Log("==> verification", s)
-
- // primary performs an equivocation attack
- var (
- latestHeight = int64(10)
- valSize = 5
- divergenceHeight = int64(6)
- primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight)
- primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
- )
- // validators don't change in this network (however we still use a map just for convenience)
- witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight+2, valSize, 2, bTime)
- witness := mockp.New(chainID, witnessHeaders, witnessValidators)
-
- for height := int64(1); height <= latestHeight; height++ {
- if height < divergenceHeight {
- primaryHeaders[height] = witnessHeaders[height]
- primaryValidators[height] = witnessValidators[height]
- continue
- }
- // we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for
- // a different block (which we do by adding txs)
- primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height,
- bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")},
- witnessValidators[height], witnessValidators[height+1], hash("app_hash"),
- hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1)
- primaryValidators[height] = witnessValidators[height]
- }
- primary := mockp.New(chainID, primaryHeaders, primaryValidators)
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: primaryHeaders[1].Hash(),
- },
- primary,
- []provider.Provider{witness},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- verificationOption,
- )
- require.NoError(t, err)
-
- // Check verification returns an error.
- _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
- if assert.Error(t, err) {
- assert.Equal(t, light.ErrLightClientAttack, err)
- }
-
- // Check evidence was sent to both full nodes.
- // Common height should be set to the height of the divergent header in the instance
- // of an equivocation attack and the validator sets are the same as what the witness has
- evAgainstPrimary := &types.LightClientAttackEvidence{
- ConflictingBlock: &types.LightBlock{
- SignedHeader: primaryHeaders[divergenceHeight],
- ValidatorSet: primaryValidators[divergenceHeight],
- },
- CommonHeight: divergenceHeight,
- }
- assert.True(t, witness.HasEvidence(evAgainstPrimary))
-
- evAgainstWitness := &types.LightClientAttackEvidence{
- ConflictingBlock: &types.LightBlock{
- SignedHeader: witnessHeaders[divergenceHeight],
- ValidatorSet: witnessValidators[divergenceHeight],
- },
- CommonHeight: divergenceHeight,
- }
- assert.True(t, primary.HasEvidence(evAgainstWitness))
- }
- }
-
- func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) {
- // primary performs a lunatic attack but changes the time of the header to
- // something in the future relative to the blockchain
- var (
- latestHeight = int64(10)
- valSize = 5
- forgedHeight = int64(12)
- proofHeight = int64(11)
- primaryHeaders = make(map[int64]*types.SignedHeader, forgedHeight)
- primaryValidators = make(map[int64]*types.ValidatorSet, forgedHeight)
- )
-
- witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight, valSize, 2, bTime)
-
- // primary has the exact same headers except it forges one extra header in the future using keys from 2/5ths of
- // the validators
- for h := range witnessHeaders {
- primaryHeaders[h] = witnessHeaders[h]
- primaryValidators[h] = witnessValidators[h]
- }
- forgedKeys := chainKeys[latestHeight].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
- primaryValidators[forgedHeight] = forgedKeys.ToValidators(2, 0)
- primaryHeaders[forgedHeight] = forgedKeys.GenSignedHeader(
- chainID,
- forgedHeight,
- bTime.Add(time.Duration(latestHeight+1)*time.Minute), // 11 mins
- nil,
- primaryValidators[forgedHeight],
- primaryValidators[forgedHeight],
- hash("app_hash"),
- hash("cons_hash"),
- hash("results_hash"),
- 0, len(forgedKeys),
- )
-
- witness := mockp.New(chainID, witnessHeaders, witnessValidators)
- primary := mockp.New(chainID, primaryHeaders, primaryValidators)
-
- laggingWitness := witness.Copy("laggingWitness")
-
- // In order to perform the attack, the primary needs at least one accomplice as a witness to also
- // send the forged block
- accomplice := primary
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: primaryHeaders[1].Hash(),
- },
- primary,
- []provider.Provider{witness, accomplice},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- light.MaxClockDrift(1*time.Second),
- light.MaxBlockLag(1*time.Second),
- )
- require.NoError(t, err)
-
- // two seconds later, the supporting withness should receive the header that can be used
- // to prove that there was an attack
- vals := chainKeys[latestHeight].ToValidators(2, 0)
- newLb := &types.LightBlock{
- SignedHeader: chainKeys[latestHeight].GenSignedHeader(
- chainID,
- proofHeight,
- bTime.Add(time.Duration(proofHeight+1)*time.Minute), // 12 mins
- nil,
- vals,
- vals,
- hash("app_hash"),
- hash("cons_hash"),
- hash("results_hash"),
- 0, len(chainKeys),
- ),
- ValidatorSet: vals,
- }
- go func() {
- time.Sleep(2 * time.Second)
- witness.AddLightBlock(newLb)
- }()
-
- // Now assert that verification returns an error. We craft the light clients time to be a little ahead of the chain
- // to allow a window for the attack to manifest itself.
- _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
- if assert.Error(t, err) {
- assert.Equal(t, light.ErrLightClientAttack, err)
- }
-
- // Check evidence was sent to the witness against the full node
- evAgainstPrimary := &types.LightClientAttackEvidence{
- ConflictingBlock: &types.LightBlock{
- SignedHeader: primaryHeaders[forgedHeight],
- ValidatorSet: primaryValidators[forgedHeight],
- },
- CommonHeight: latestHeight,
- }
- assert.True(t, witness.HasEvidence(evAgainstPrimary))
-
- // We attempt the same call but now the supporting witness has a block which should
- // immediately conflict in time with the primary
- _, err = c.VerifyLightBlockAtHeight(ctx, forgedHeight, bTime.Add(time.Duration(forgedHeight)*time.Minute))
- if assert.Error(t, err) {
- assert.Equal(t, light.ErrLightClientAttack, err)
- }
- assert.True(t, witness.HasEvidence(evAgainstPrimary))
-
- // Lastly we test the unfortunate case where the light clients supporting witness doesn't update
- // in enough time
- c, err = light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Period: 4 * time.Hour,
- Height: 1,
- Hash: primaryHeaders[1].Hash(),
- },
- primary,
- []provider.Provider{laggingWitness, accomplice},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- light.MaxClockDrift(1*time.Second),
- light.MaxBlockLag(1*time.Second),
- )
- require.NoError(t, err)
-
- _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
- assert.NoError(t, err)
-
- }
-
- // 1. Different nodes therefore a divergent header is produced.
- // => light client returns an error upon creation because primary and witness
- // have a different view.
- func TestClientDivergentTraces1(t *testing.T) {
- primary := mockp.New(genMockNode(chainID, 10, 5, 2, bTime))
- firstBlock, err := primary.LightBlock(ctx, 1)
- require.NoError(t, err)
- witness := mockp.New(genMockNode(chainID, 10, 5, 2, bTime))
-
- _, err = light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Height: 1,
- Hash: firstBlock.Hash(),
- Period: 4 * time.Hour,
- },
- primary,
- []provider.Provider{witness},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.Error(t, err)
- assert.Contains(t, err.Error(), "does not match primary")
- }
-
- // 2. Two out of three nodes don't respond but the third has a header that matches
- // => verification should be successful and all the witnesses should remain
- func TestClientDivergentTraces2(t *testing.T) {
- primary := mockp.New(genMockNode(chainID, 10, 5, 2, bTime))
- firstBlock, err := primary.LightBlock(ctx, 1)
- require.NoError(t, err)
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Height: 1,
- Hash: firstBlock.Hash(),
- Period: 4 * time.Hour,
- },
- primary,
- []provider.Provider{deadNode, deadNode, primary},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
- assert.NoError(t, err)
- assert.Equal(t, 3, len(c.Witnesses()))
- }
-
- // 3. witness has the same first header, but different second header
- // => creation should succeed, but the verification should fail
- func TestClientDivergentTraces3(t *testing.T) {
- _, primaryHeaders, primaryVals := genMockNode(chainID, 10, 5, 2, bTime)
- primary := mockp.New(chainID, primaryHeaders, primaryVals)
-
- firstBlock, err := primary.LightBlock(ctx, 1)
- require.NoError(t, err)
-
- _, mockHeaders, mockVals := genMockNode(chainID, 10, 5, 2, bTime)
- mockHeaders[1] = primaryHeaders[1]
- mockVals[1] = primaryVals[1]
- witness := mockp.New(chainID, mockHeaders, mockVals)
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Height: 1,
- Hash: firstBlock.Hash(),
- Period: 4 * time.Hour,
- },
- primary,
- []provider.Provider{witness},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
- assert.Error(t, err)
- assert.Equal(t, 1, len(c.Witnesses()))
- }
-
- // 4. Witness has a divergent header but can not produce a valid trace to back it up.
- // It should be ignored
- func TestClientDivergentTraces4(t *testing.T) {
- _, primaryHeaders, primaryVals := genMockNode(chainID, 10, 5, 2, bTime)
- primary := mockp.New(chainID, primaryHeaders, primaryVals)
-
- firstBlock, err := primary.LightBlock(ctx, 1)
- require.NoError(t, err)
-
- _, mockHeaders, mockVals := genMockNode(chainID, 10, 5, 2, bTime)
- witness := primary.Copy("witness")
- witness.AddLightBlock(&types.LightBlock{
- SignedHeader: mockHeaders[10],
- ValidatorSet: mockVals[10],
- })
-
- c, err := light.NewClient(
- ctx,
- chainID,
- light.TrustOptions{
- Height: 1,
- Hash: firstBlock.Hash(),
- Period: 4 * time.Hour,
- },
- primary,
- []provider.Provider{witness},
- dbs.New(dbm.NewMemDB()),
- light.Logger(log.TestingLogger()),
- )
- require.NoError(t, err)
-
- _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
- assert.Error(t, err)
- assert.Equal(t, 1, len(c.Witnesses()))
- }
|