diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 866ebd018..eb262d129 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -338,8 +338,30 @@ func TestCheckEvidenceWithLightClientAttack(t *testing.T) { // Take away the last signature -> there are less validators then what we have detected, // hence this should fail. - commit.Signatures = append(commit.Signatures[:nValidators-1], types.NewCommitSigAbsent()) - require.Error(t, pool.CheckEvidence(types.EvidenceList{ev})) + newSigs := append(commit.Signatures[:nValidators-2], types.NewCommitSigAbsent(), types.NewCommitSigAbsent()) + newCommit := types.NewCommit(commit.Height, commit.Round, commit.BlockID, newSigs) + differentEv := &types.LightClientAttackEvidence{ + ConflictingBlock: &types.LightBlock{ + SignedHeader: &types.SignedHeader{ + Header: conflictingHeader, + Commit: newCommit, + }, + ValidatorSet: conflictingVals, + }, + CommonHeight: 10, + TotalVotingPower: int64(nValidators) * validatorPower, + ByzantineValidators: conflictingVals.Validators, + Timestamp: defaultEvidenceTime, + } + require.Error(t, pool.CheckEvidence(types.EvidenceList{differentEv})) + + state.LastBlockHeight++ + pool.Update(state, types.EvidenceList{ev}) + + require.NoError(t, pool.AddEvidence(differentEv)) + require.NoError(t, pool.AddEvidence(ev)) + pendingEv, _ := pool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes) + require.Empty(t, pendingEv) } // Tests that restarting the evidence pool after a potential failure will recover the diff --git a/types/evidence.go b/types/evidence.go index 7284018c2..305a645bb 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -317,8 +317,14 @@ func (l *LightClientAttackEvidence) Height() int64 { // String returns a string representation of LightClientAttackEvidence func (l *LightClientAttackEvidence) String() string { - return fmt.Sprintf("LightClientAttackEvidence{ConflictingBlock: %v, CommonHeight: %d}", - l.ConflictingBlock.String(), l.CommonHeight) + return fmt.Sprintf(`LightClientAttackEvidence{ + ConflictingBlock: %v, + CommonHeight: %d, + ByzatineValidators: %v, + TotalVotingPower: %d, + Timestamp: %v}#%X`, + l.ConflictingBlock.String(), l.CommonHeight, l.ByzantineValidators, + l.TotalVotingPower, l.Timestamp, l.Hash()) } // Time returns the time of the common block where the infraction leveraged off. @@ -337,20 +343,24 @@ func (l *LightClientAttackEvidence) ValidateBasic() error { return errors.New("conflicting block missing header") } - if err := l.ConflictingBlock.ValidateBasic(l.ConflictingBlock.ChainID); err != nil { - return fmt.Errorf("invalid conflicting light block: %w", err) + if l.TotalVotingPower <= 0 { + return errors.New("negative or zero total voting power") } if l.CommonHeight <= 0 { return errors.New("negative or zero common height") } - + // check that common height isn't ahead of the height of the conflicting block. It // is possible that they are the same height if the light node witnesses either an // amnesia or a equivocation attack. if l.CommonHeight > l.ConflictingBlock.Height { return fmt.Errorf("common height is ahead of the conflicting block height (%d > %d)", - l.CommonHeight, l.ConflictingBlock.Height) + l.CommonHeight, l.ConflictingBlock.Height) + } + + if err := l.ConflictingBlock.ValidateBasic(l.ConflictingBlock.ChainID); err != nil { + return fmt.Errorf("invalid conflicting light block: %w", err) } return nil diff --git a/types/evidence_test.go b/types/evidence_test.go index f50ed0b18..02828eaaa 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -89,9 +89,11 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { } } -func TestLightClientAttackEvidence(t *testing.T) { +func TestLightClientAttackEvidenceBasic(t *testing.T) { height := int64(5) - voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, 10, 1) + commonHeight := height - 1 + nValidators := 10 + voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, nValidators, 1) header := makeHeaderRandom() header.Height = height blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash"))) @@ -105,56 +107,64 @@ func TestLightClientAttackEvidence(t *testing.T) { }, ValidatorSet: valSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: valSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], } assert.NotNil(t, lcae.String()) assert.NotNil(t, lcae.Hash()) - // only 7 validators sign - differentCommit, err := MakeCommit(blockID, height, 1, voteSet, privVals[:7], defaultVoteTime) - require.NoError(t, err) - differentEv := &LightClientAttackEvidence{ - ConflictingBlock: &LightBlock{ - SignedHeader: &SignedHeader{ - Header: header, - Commit: differentCommit, - }, - ValidatorSet: valSet, - }, - CommonHeight: height - 1, - } - assert.Equal(t, lcae.Hash(), differentEv.Hash()) - // different header hash - differentHeader := makeHeaderRandom() - differentEv = &LightClientAttackEvidence{ - ConflictingBlock: &LightBlock{ - SignedHeader: &SignedHeader{ - Header: differentHeader, - Commit: differentCommit, - }, - ValidatorSet: valSet, - }, - CommonHeight: height - 1, - } - assert.NotEqual(t, lcae.Hash(), differentEv.Hash()) - // different common height should produce a different header - differentEv = &LightClientAttackEvidence{ - ConflictingBlock: &LightBlock{ - SignedHeader: &SignedHeader{ - Header: header, - Commit: differentCommit, - }, - ValidatorSet: valSet, - }, - CommonHeight: height - 2, - } - assert.NotEqual(t, lcae.Hash(), differentEv.Hash()) - assert.Equal(t, lcae.Height(), int64(4)) // Height should be the common Height + assert.Equal(t, lcae.Height(), commonHeight) // Height should be the common Height assert.NotNil(t, lcae.Bytes()) + + // maleate evidence to test hash uniqueness + testCases := []struct { + testName string + malleateEvidence func(*LightClientAttackEvidence) + }{ + {"Different header", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock.Header = makeHeaderRandom() }}, + {"Different commit", func(ev *LightClientAttackEvidence) { + newSigs := append(commit.Signatures[:len(commit.Signatures)-1], NewCommitSigAbsent()) + newCommit := NewCommit(height, 0, blockID, newSigs) + require.NotEqual(t, commit.Hash(), newCommit.Hash()) + ev.ConflictingBlock.Commit = newCommit + }}, + {"Different common height", func(ev *LightClientAttackEvidence) { + ev.CommonHeight = height + 1 + }}, + {"Different total voting power", func(ev *LightClientAttackEvidence) { ev.TotalVotingPower *= 2 }}, + {"Different timestamp", func(ev *LightClientAttackEvidence) { ev.Timestamp = header.Time.Add(1 * time.Hour) }}, + {"Different byzantine validators", func(ev *LightClientAttackEvidence) { + ev.ByzantineValidators = []*Validator{} + }}, + } + + for _, tc := range testCases { + lcae := &LightClientAttackEvidence{ + ConflictingBlock: &LightBlock{ + SignedHeader: &SignedHeader{ + Header: header, + Commit: commit, + }, + ValidatorSet: valSet, + }, + CommonHeight: commonHeight, + TotalVotingPower: valSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], + } + hash := lcae.Hash() + t.Log(hash) + tc.malleateEvidence(lcae) + assert.NotEqual(t, hash, lcae.Hash(), tc.testName) + } } func TestLightClientAttackEvidenceValidation(t *testing.T) { height := int64(5) - voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, 10, 1) + commonHeight := height - 1 + nValidators := 10 + voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, nValidators, 1) header := makeHeaderRandom() header.Height = height header.ValidatorsHash = valSet.Hash() @@ -169,7 +179,10 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) { }, ValidatorSet: valSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: valSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], } assert.NoError(t, lcae.ValidateBasic()) @@ -178,16 +191,22 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) { malleateEvidence func(*LightClientAttackEvidence) expectErr bool }{ - {"Good DuplicateVoteEvidence", func(ev *LightClientAttackEvidence) {}, false}, + {"Good LightClientAttackEvidence", func(ev *LightClientAttackEvidence) {}, false}, {"Negative height", func(ev *LightClientAttackEvidence) { ev.CommonHeight = -10 }, true}, {"Height is greater than divergent block", func(ev *LightClientAttackEvidence) { ev.CommonHeight = height + 1 }, true}, + {"Height is equal to the divergent block", func(ev *LightClientAttackEvidence) { + ev.CommonHeight = height + }, false}, {"Nil conflicting header", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock.Header = nil }, true}, {"Nil conflicting blocl", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock = nil }, true}, {"Nil validator set", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock.ValidatorSet = &ValidatorSet{} }, true}, + {"Negative total voting power", func(ev *LightClientAttackEvidence) { + ev.TotalVotingPower = -1 + }, true}, } for _, tc := range testCases { tc := tc @@ -200,7 +219,10 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) { }, ValidatorSet: valSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: valSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], } tc.malleateEvidence(lcae) if tc.expectErr { diff --git a/types/light_test.go b/types/light_test.go index f4e1e52df..f3e0af1a6 100644 --- a/types/light_test.go +++ b/types/light_test.go @@ -152,11 +152,13 @@ func TestSignedHeaderValidateBasic(t *testing.T) { Header: tc.shHeader, Commit: tc.shCommit, } - assert.Equal( + err := sh.ValidateBasic(validSignedHeader.Header.ChainID) + assert.Equalf( t, tc.expectErr, - sh.ValidateBasic(validSignedHeader.Header.ChainID) != nil, + err != nil, "Validate Basic had an unexpected result", + err, ) }) }