From 5d58fc34b11ec2b9e163de6ea8da609c4e5a7122 Mon Sep 17 00:00:00 2001 From: tnasu Date: Fri, 7 Jan 2022 15:40:12 +0900 Subject: [PATCH] evidence: fix bug with hashes (backport #6375) (#6381) --- evidence/pool.go | 71 +----- evidence/pool_test.go | 99 ++++----- evidence/verify.go | 101 +++++---- evidence/verify_test.go | 416 ++++++++++++++++++++++------------- test/e2e/tests/block_test.go | 6 +- types/evidence.go | 43 ++-- types/evidence_test.go | 100 +++++---- types/light.go | 2 +- types/light_test.go | 6 +- types/validator.go | 2 +- 10 files changed, 470 insertions(+), 376 deletions(-) diff --git a/evidence/pool.go b/evidence/pool.go index 373108cac..a2640ebd1 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "sort" "sync" "sync/atomic" "time" @@ -194,9 +193,11 @@ func (evpool *Pool) CheckEvidence(evList types.EvidenceList) error { hashes := make([][]byte, len(evList)) for idx, ev := range evList { - ok := evpool.fastCheck(ev) + _, isLightEv := ev.(*types.LightClientAttackEvidence) - if !ok { + // We must verify light client attack evidence regardless because there could be a + // different conflicting block with the same hash. + if isLightEv || !evpool.isPending(ev) { // check that the evidence isn't already committed if evpool.isCommitted(ev) { return &types.ErrInvalidEvidence{Evidence: ev, Reason: errors.New("evidence was already committed")} @@ -213,7 +214,7 @@ func (evpool *Pool) CheckEvidence(evList types.EvidenceList) error { evpool.logger.Error("Can't add evidence to pending list", "err", err, "ev", ev) } - evpool.logger.Info("Verified new evidence of byzantine behavior", "evidence", ev) + evpool.logger.Info("Check evidence: verified evidence of byzantine behavior", "evidence", ev) } // check for duplicate evidence. We cache hashes so we don't have to work them out again. @@ -255,68 +256,6 @@ func (evpool *Pool) State() sm.State { return evpool.state } -//-------------------------------------------------------------------------- - -// fastCheck leverages the fact that the evidence pool may have already verified the evidence to see if it can -// quickly conclude that the evidence is already valid. -func (evpool *Pool) fastCheck(ev types.Evidence) bool { - if lcae, ok := ev.(*types.LightClientAttackEvidence); ok { - key := keyPending(ev) - evBytes, err := evpool.evidenceStore.Get(key) - if evBytes == nil { // the evidence is not in the nodes pending list - return false - } - if err != nil { - evpool.logger.Error("Failed to load light client attack evidence", "err", err, "key(height/hash)", key) - return false - } - var trustedPb tmproto.LightClientAttackEvidence - err = trustedPb.Unmarshal(evBytes) - if err != nil { - evpool.logger.Error("Failed to convert light client attack evidence from bytes", - "err", err, "key(height/hash)", key) - return false - } - trustedEv, err := types.LightClientAttackEvidenceFromProto(&trustedPb) - if err != nil { - evpool.logger.Error("Failed to convert light client attack evidence from protobuf", - "err", err, "key(height/hash)", key) - return false - } - // ensure that all the byzantine validators that the evidence pool has match the byzantine validators - // in this evidence - if trustedEv.ByzantineValidators == nil && lcae.ByzantineValidators != nil { - return false - } - - if len(trustedEv.ByzantineValidators) != len(lcae.ByzantineValidators) { - return false - } - - byzValsCopy := make([]*types.Validator, len(lcae.ByzantineValidators)) - for i, v := range lcae.ByzantineValidators { - byzValsCopy[i] = v.Copy() - } - - // ensure that both validator arrays are in the same order - sort.Sort(types.ValidatorsByVotingPower(byzValsCopy)) - - for idx, val := range trustedEv.ByzantineValidators { - if !bytes.Equal(byzValsCopy[idx].Address, val.Address) { - return false - } - if byzValsCopy[idx].StakingPower != val.StakingPower { - return false - } - } - - return true - } - - // for all other evidence the evidence pool just checks if it is already in the pending db - return evpool.isPending(ev) -} - // IsExpired checks whether evidence or a polc is expired by checking whether a height and time is older // than set by the evidence consensus parameters func (evpool *Pool) isExpired(height int64, time time.Time) bool { diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 385bfb934..a0aa1659d 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -198,6 +198,9 @@ func TestReportConflictingVotes(t *testing.T) { // should be able to retrieve evidence from pool evList, _ = pool.PendingEvidence(defaultEvidenceMaxBytes) require.Equal(t, []types.Evidence{ev}, evList) + + next = pool.EvidenceFront() + require.NotNil(t, next) } func TestEvidencePoolUpdate(t *testing.T) { @@ -258,64 +261,32 @@ func TestVerifyDuplicatedEvidenceFails(t *testing.T) { // check that valid light client evidence is correctly validated and stored in // evidence pool -func TestCheckEvidenceWithLightClientAttack(t *testing.T) { +func TestLightClientAttackEvidenceLifecycle(t *testing.T) { var ( - nValidators = 5 - validatorPower int64 = 10 - height int64 = 10 + height int64 = 100 + commonHeight int64 = 90 ) - conflictingVals, conflictingVoters, conflictingPrivVals := types.RandVoterSet(nValidators, validatorPower) - trustedHeader := makeHeaderRandom(height) - trustedHeader.Time = defaultEvidenceTime - - conflictingHeader := makeHeaderRandom(height) - conflictingHeader.VotersHash = conflictingVoters.Hash() - - trustedHeader.VotersHash = conflictingHeader.VotersHash - trustedHeader.NextValidatorsHash = conflictingHeader.NextValidatorsHash - trustedHeader.ConsensusHash = conflictingHeader.ConsensusHash - trustedHeader.AppHash = conflictingHeader.AppHash - trustedHeader.LastResultsHash = conflictingHeader.LastResultsHash - - // for simplicity we are simulating a duplicate vote attack where all the validators in the - // conflictingVals set voted twice - blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) - voteSet := types.NewVoteSet(evidenceChainID, height, 1, tmproto.SignedMsgType(2), conflictingVoters) - commit, err := types.MakeCommit(blockID, height, 1, voteSet, conflictingPrivVals, defaultEvidenceTime) - require.NoError(t, err) - ev := &types.LightClientAttackEvidence{ - ConflictingBlock: &types.LightBlock{ - SignedHeader: &types.SignedHeader{ - Header: conflictingHeader, - Commit: commit, - }, - ValidatorSet: conflictingVals, - VoterSet: conflictingVoters, - }, - CommonHeight: height, - TotalVotingPower: int64(nValidators) * validatorPower, - ByzantineValidators: conflictingVals.Validators, - Timestamp: defaultEvidenceTime, - } - - trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) - trustedVoteSet := types.NewVoteSet(evidenceChainID, height, 1, tmproto.SignedMsgType(2), conflictingVoters) - trustedCommit, err := types.MakeCommit(trustedBlockID, height, 1, trustedVoteSet, conflictingPrivVals, - defaultEvidenceTime) - require.NoError(t, err) + ev, trusted, common := makeLunaticEvidence(t, height, commonHeight, + 10, 5, 5, defaultEvidenceTime, defaultEvidenceTime.Add(1*time.Hour)) state := sm.State{ - LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute), - LastBlockHeight: 11, + LastBlockTime: defaultEvidenceTime.Add(2 * time.Hour), + LastBlockHeight: 110, ConsensusParams: *types.DefaultConsensusParams(), } stateStore := &smmocks.Store{} - stateStore.On("LoadValidators", height).Return(conflictingVals, nil) - stateStore.On("LoadVoters", height, mock.AnythingOfType("*types.VoterParams")).Return(conflictingVoters, nil) + stateStore.On("LoadValidators", height).Return(trusted.ValidatorSet, nil) + stateStore.On("LoadValidators", commonHeight).Return(common.ValidatorSet, nil) + stateStore.On("LoadVoters", height, mock.AnythingOfType("*types.VoterParams")).Return( + ev.ConflictingBlock.VoterSet, nil) // Should use correct VoterSet for bls.VerifyAggregatedSignature + stateStore.On("LoadVoters", commonHeight, mock.AnythingOfType("*types.VoterParams")).Return( + ev.ConflictingBlock.VoterSet, nil) // Should use correct VoterSet for bls.VerifyAggregatedSignature stateStore.On("Load").Return(state, nil) blockStore := &mocks.BlockStore{} - blockStore.On("LoadBlockMeta", height).Return(&types.BlockMeta{Header: *trustedHeader}) - blockStore.On("LoadBlockCommit", height).Return(trustedCommit) + blockStore.On("LoadBlockMeta", height).Return(&types.BlockMeta{Header: *trusted.Header}) + blockStore.On("LoadBlockMeta", commonHeight).Return(&types.BlockMeta{Header: *common.Header}) + blockStore.On("LoadBlockCommit", height).Return(trusted.Commit) + blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit) pool, err := evidence.NewPool(memdb.NewDB(), stateStore, blockStore) require.NoError(t, err) @@ -324,8 +295,32 @@ func TestCheckEvidenceWithLightClientAttack(t *testing.T) { err = pool.AddEvidence(ev) assert.NoError(t, err) - err = pool.CheckEvidence(types.EvidenceList{ev}) - assert.NoError(t, err) + hash := ev.Hash() + + require.NoError(t, pool.AddEvidence(ev)) + require.NoError(t, pool.AddEvidence(ev)) + + pendingEv, _ := pool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes) + require.Equal(t, 1, len(pendingEv)) + require.Equal(t, ev, pendingEv[0]) + + require.NoError(t, pool.CheckEvidence(pendingEv)) + require.Equal(t, ev, pendingEv[0]) + + state.LastBlockHeight++ + state.LastBlockTime = state.LastBlockTime.Add(1 * time.Minute) + pool.Update(state, pendingEv) + require.Equal(t, hash, pendingEv[0].Hash()) + + remaindingEv, _ := pool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes) + require.Empty(t, remaindingEv) + + // evidence is already committed so it shouldn't pass + require.Error(t, pool.CheckEvidence(types.EvidenceList{ev})) + require.NoError(t, pool.AddEvidence(ev)) + + remaindingEv, _ = pool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes) + require.Empty(t, remaindingEv) } // Tests that restarting the evidence pool after a potential failure will recover the @@ -365,7 +360,7 @@ func TestRecoverPendingEvidence(t *testing.T) { Evidence: tmproto.EvidenceParams{ MaxAgeNumBlocks: 20, MaxAgeDuration: 20 * time.Minute, - MaxBytes: 1000, + MaxBytes: defaultEvidenceMaxBytes, }, }, }, nil) diff --git a/evidence/verify.go b/evidence/verify.go index 228988d66..b68871f15 100644 --- a/evidence/verify.go +++ b/evidence/verify.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "sort" "time" "github.com/line/ostracon/light" @@ -98,34 +97,6 @@ func (evpool *Pool) verify(evidence types.Evidence) error { if err != nil { return err } - // find out what type of attack this was and thus extract the malicious validators. Note in the case of an - // Amnesia attack we don't have any malicious validators. - validators := ev.GetByzantineValidators(commonVoters, trustedHeader) - // ensure this matches the validators that are listed in the evidence. They should be ordered based on power. - if validators == nil && ev.ByzantineValidators != nil { - return fmt.Errorf("expected nil validators from an amnesia light client attack but got %d", - len(ev.ByzantineValidators)) - } - - if exp, got := len(validators), len(ev.ByzantineValidators); exp != got { - return fmt.Errorf("expected %d byzantine validators from evidence but got %d", - exp, got) - } - - // ensure that both validator arrays are in the same order - sort.Sort(types.ValidatorsByVotingPower(ev.ByzantineValidators)) - - for idx, val := range validators { - if !bytes.Equal(ev.ByzantineValidators[idx].Address, val.Address) { - return fmt.Errorf("evidence contained a different byzantine validator address to the one we were expecting."+ - "Expected %v, got %v", val.Address, ev.ByzantineValidators[idx].Address) - } - if ev.ByzantineValidators[idx].StakingPower != val.StakingPower { - return fmt.Errorf("evidence contained a byzantine validator with a different power to the one we were expecting."+ - "Expected %d, got %d", val.StakingPower, ev.ByzantineValidators[idx].StakingPower) - } - } - return nil default: return fmt.Errorf("unrecognized evidence type: %T", evidence) @@ -161,7 +132,7 @@ func VerifyLightClientAttack( } // In the case of equivocation and amnesia we expect all header hashes to be correctly derived - } else if isInvalidHeader(trustedHeader.Header, e.ConflictingBlock.Header) { + } else if e.ConflictingHeaderIsInvalid(trustedHeader.Header) { return errors.New("common height is the same as conflicting block height so expected the conflicting" + " block to be correctly derived yet it wasn't") } @@ -191,7 +162,7 @@ func VerifyLightClientAttack( trustedHeader.Hash()) } - return nil + return validateABCIEvidence(e, commonVoters, trustedHeader) } // VerifyDuplicateVote verifies DuplicateVoteEvidence against the state of full node. This involves the @@ -262,6 +233,62 @@ func VerifyDuplicateVote(e *types.DuplicateVoteEvidence, chainID string, voterSe return nil } +// validateABCIEvidence validates the ABCI component of the light client attack +// evidence i.e voting power and byzantine validators +func validateABCIEvidence( + ev *types.LightClientAttackEvidence, + commonVoter *types.VoterSet, + trustedHeader *types.SignedHeader, +) error { + if evTotal, valsTotal := ev.TotalVotingPower, commonVoter.TotalVotingPower(); evTotal != valsTotal { + return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", + evTotal, valsTotal) + } + + // Find out what type of attack this was and thus extract the malicious + // validators. Note, in the case of an Amnesia attack we don't have any + // malicious validators. + validators := ev.GetByzantineValidators(commonVoter, trustedHeader) + + // Ensure this matches the validators that are listed in the evidence. They + // should be ordered based on power. + if validators == nil && ev.ByzantineValidators != nil { + return fmt.Errorf( + "expected nil validators from an amnesia light client attack but got %d", + len(ev.ByzantineValidators), + ) + } + + if exp, got := len(validators), len(ev.ByzantineValidators); exp != got { + return fmt.Errorf("expected %d byzantine validators from evidence but got %d", exp, got) + } + + for idx, val := range validators { + if !bytes.Equal(ev.ByzantineValidators[idx].Address, val.Address) { + return fmt.Errorf( + "evidence contained an unexpected byzantine validator address; expected: %v, got: %v", + val.Address, ev.ByzantineValidators[idx].Address, + ) + } + + if ev.ByzantineValidators[idx].StakingPower != val.StakingPower { + return fmt.Errorf( + "evidence contained unexpected byzantine validator staking power; expected %d, got %d", + val.StakingPower, ev.ByzantineValidators[idx].StakingPower, + ) + } + + if ev.ByzantineValidators[idx].VotingPower != val.VotingPower { + return fmt.Errorf( + "evidence contained unexpected byzantine validator voting power; expected %d, got %d", + val.VotingPower, ev.ByzantineValidators[idx].VotingPower, + ) + } + } + + return nil +} + func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, error) { blockMeta := blockStore.LoadBlockMeta(height) if blockMeta == nil { @@ -276,15 +303,3 @@ func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, Commit: commit, }, nil } - -// isInvalidHeader takes a trusted header and matches it againt a conflicting header -// to determine whether the conflicting header was the product of a valid state transition -// or not. If it is then all the deterministic fields of the header should be the same. -// If not, it is an invalid header and constitutes a lunatic attack. -func isInvalidHeader(trusted, conflicting *types.Header) bool { - return !bytes.Equal(trusted.VotersHash, conflicting.VotersHash) || - !bytes.Equal(trusted.NextValidatorsHash, conflicting.NextValidatorsHash) || - !bytes.Equal(trusted.ConsensusHash, conflicting.ConsensusHash) || - !bytes.Equal(trusted.AppHash, conflicting.AppHash) || - !bytes.Equal(trusted.LastResultsHash, conflicting.LastResultsHash) -} diff --git a/evidence/verify_test.go b/evidence/verify_test.go index 925d197b7..b604df070 100644 --- a/evidence/verify_test.go +++ b/evidence/verify_test.go @@ -1,9 +1,13 @@ package evidence_test import ( + "bytes" + "sort" "testing" "time" + "github.com/line/ostracon/light" + "github.com/coniks-sys/coniks-go/crypto/vrf" "github.com/line/tm-db/v2/memdb" @@ -25,190 +29,181 @@ import ( "github.com/line/ostracon/version" ) -func TestVerifyLightClientAttack_Lunatic(t *testing.T) { - - commonVals, commonVoters, commonPrivVals := types.RandVoterSet(2, 10) - // use the correct Proof to bypass the checks in libsodium - var proof []byte - proof, err := commonPrivVals[0].GenerateVRFProof([]byte{}) - require.NoError(t, err) - - newVal, newPrivVal := types.RandValidatorForPrivKey(types.PrivKeyEd25519, false, 9) - - conflictingVals, err := types.ValidatorSetFromExistingValidators(append(commonVals.Validators, newVal)) - require.NoError(t, err) - conflictingVoters, err := types.ValidatorSetFromExistingValidators(append(commonVoters.Voters, newVal)) - require.NoError(t, err) - conflictingVoterSet := types.ToVoterAll(conflictingVoters.Validators) - conflictingPrivVals := append(commonPrivVals, newPrivVal) - - commonHeader := makeHeaderRandom(4) - commonHeader.Time = defaultEvidenceTime - commonHeader.Proof = proof - trustedHeader := makeHeaderRandom(10) - trustedHeader.Time = defaultEvidenceTime.Add(1 * time.Hour) - - conflictingHeader := makeHeaderRandom(10) - conflictingHeader.Time = defaultEvidenceTime.Add(1 * time.Hour) - conflictingHeader.ValidatorsHash = conflictingVals.Hash() - conflictingHeader.VotersHash = conflictingVoterSet.Hash() - conflictingHeader.Proof = proof - - // we are simulating a lunatic light client attack - blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) - voteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVoterSet) - commit, err := types.MakeCommit(blockID, 10, 1, voteSet, conflictingPrivVals, defaultEvidenceTime) - require.NoError(t, err) - ev := &types.LightClientAttackEvidence{ - ConflictingBlock: &types.LightBlock{ - SignedHeader: &types.SignedHeader{ - Header: conflictingHeader, - Commit: commit, - }, - ValidatorSet: conflictingVals, - VoterSet: conflictingVoterSet, - }, - CommonHeight: 4, - TotalVotingPower: 20, - ByzantineValidators: commonVals.Validators, - Timestamp: defaultEvidenceTime, - } - - commonSignedHeader := &types.SignedHeader{ - Header: commonHeader, - Commit: &types.Commit{}, - } - commonSignedHeader.Proof = proof +const ( + defaultVotingPower = 10 +) - trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) - _, voters, privVals := types.RandVoterSet(3, 8) - trustedVoteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), voters) - trustedCommit, err := types.MakeCommit(trustedBlockID, 10, 1, trustedVoteSet, privVals, defaultEvidenceTime) - require.NoError(t, err) - trustedSignedHeader := &types.SignedHeader{ - Header: trustedHeader, - Commit: trustedCommit, - } +func TestVerifyLightClientAttack_Lunatic(t *testing.T) { + const ( + height int64 = 10 + commonHeight int64 = 4 + totalVals = 10 + byzVals = 4 + ) + attackTime := defaultEvidenceTime.Add(1 * time.Hour) + // create valid lunatic evidence + ev, trusted, common := makeLunaticEvidence( + t, height, commonHeight, totalVals, byzVals, totalVals-byzVals, defaultEvidenceTime, attackTime) + require.NoError(t, ev.ValidateBasic()) // good pass -> no error - err = evidence.VerifyLightClientAttack( - ev, - commonSignedHeader, - trustedSignedHeader, - commonVals, - commonVoters, - defaultEvidenceTime.Add(2*time.Hour), - 3*time.Hour, - types.DefaultVoterParams(), - ) + err := evidence.VerifyLightClientAttack(ev, common.SignedHeader, trusted.SignedHeader, + common.ValidatorSet, + ev.ConflictingBlock.VoterSet, // Should use correct VoterSet for bls.VerifyAggregatedSignature + defaultEvidenceTime.Add(2*time.Hour), 3*time.Hour, types.DefaultVoterParams()) assert.NoError(t, err) // trusted and conflicting hashes are the same -> an error should be returned - err = evidence.VerifyLightClientAttack( - ev, - commonSignedHeader, - ev.ConflictingBlock.SignedHeader, - commonVals, - commonVoters, - defaultEvidenceTime.Add(2*time.Hour), - 3*time.Hour, - types.DefaultVoterParams(), - ) + err = evidence.VerifyLightClientAttack(ev, common.SignedHeader, ev.ConflictingBlock.SignedHeader, + common.ValidatorSet, common.VoterSet, + defaultEvidenceTime.Add(2*time.Hour), 3*time.Hour, types.DefaultVoterParams()) assert.Error(t, err) // evidence with different total validator power should fail - ev.TotalVotingPower = 1 - err = evidence.VerifyLightClientAttack( - ev, - commonSignedHeader, - trustedSignedHeader, - commonVals, - commonVoters, - defaultEvidenceTime.Add(2*time.Hour), - 3*time.Hour, - types.DefaultVoterParams(), - ) + ev.TotalVotingPower = 1 * defaultVotingPower + err = evidence.VerifyLightClientAttack(ev, common.SignedHeader, trusted.SignedHeader, + common.ValidatorSet, common.VoterSet, + defaultEvidenceTime.Add(2*time.Hour), 3*time.Hour, types.DefaultVoterParams()) assert.Error(t, err) - ev.TotalVotingPower = 20 - - forwardConflictingHeader := makeHeaderRandom(11) - forwardConflictingHeader.Time = defaultEvidenceTime.Add(30 * time.Minute) - forwardConflictingHeader.ValidatorsHash = conflictingVals.Hash() - forwardConflictingHeader.VotersHash = conflictingVoterSet.Hash() - forwardBlockID := makeBlockID(forwardConflictingHeader.Hash(), 1000, []byte("partshash")) - forwardVoteSet := types.NewVoteSet(evidenceChainID, 11, 1, tmproto.SignedMsgType(2), conflictingVoterSet) - forwardCommit, err := types.MakeCommit(forwardBlockID, 11, 1, forwardVoteSet, conflictingPrivVals, defaultEvidenceTime) - require.NoError(t, err) - forwardLunaticEv := &types.LightClientAttackEvidence{ - ConflictingBlock: &types.LightBlock{ - SignedHeader: &types.SignedHeader{ - Header: forwardConflictingHeader, - Commit: forwardCommit, - }, - ValidatorSet: conflictingVals, - VoterSet: conflictingVoterSet, - }, - CommonHeight: 4, - TotalVotingPower: 20, - ByzantineValidators: commonVals.Validators, - Timestamp: defaultEvidenceTime, - } - err = evidence.VerifyLightClientAttack( - forwardLunaticEv, - commonSignedHeader, - trustedSignedHeader, - commonVals, - commonVoters, - defaultEvidenceTime.Add(2*time.Hour), - 3*time.Hour, - types.DefaultVoterParams(), + + // evidence without enough malicious votes should fail + ev, trusted, common = makeLunaticEvidence( + t, height, commonHeight, totalVals, byzVals-1, totalVals-byzVals, defaultEvidenceTime, attackTime) + err = evidence.VerifyLightClientAttack(ev, common.SignedHeader, trusted.SignedHeader, + common.ValidatorSet, common.VoterSet, + defaultEvidenceTime.Add(2*time.Hour), 3*time.Hour, types.DefaultVoterParams()) + assert.Error(t, err) +} + +func TestVerify_LunaticAttackAgainstState(t *testing.T) { + const ( + height int64 = 10 + commonHeight int64 = 4 + totalVals = 10 + byzVals = 4 ) - assert.NoError(t, err) + attackTime := defaultEvidenceTime.Add(1 * time.Hour) + // create valid lunatic evidence + ev, trusted, common := makeLunaticEvidence( + t, height, commonHeight, totalVals, byzVals, totalVals-byzVals, defaultEvidenceTime, attackTime) + // now we try to test verification against state state := sm.State{ LastBlockTime: defaultEvidenceTime.Add(2 * time.Hour), - LastBlockHeight: 11, + LastBlockHeight: height + 1, ConsensusParams: *types.DefaultConsensusParams(), VoterParams: types.DefaultVoterParams(), } stateStore := &smmocks.Store{} - stateStore.On("LoadValidators", int64(4)).Return(commonVals, nil) - stateStore.On("LoadVoters", int64(4), mock.AnythingOfType("*types.VoterParams")).Return(commonVoters, nil) + stateStore.On("LoadValidators", commonHeight).Return(common.ValidatorSet, nil) + stateStore.On("LoadVoters", commonHeight, mock.AnythingOfType("*types.VoterParams")).Return( + ev.ConflictingBlock.VoterSet, nil) // Should use correct VoterSet for bls.VerifyAggregatedSignature stateStore.On("Load").Return(state, nil) blockStore := &mocks.BlockStore{} - blockStore.On("LoadBlockMeta", int64(4)).Return(&types.BlockMeta{Header: *commonHeader}) - blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader}) - blockStore.On("LoadBlockMeta", int64(11)).Return(nil) - blockStore.On("LoadBlockCommit", int64(4)).Return(commit) - blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit) - blockStore.On("Height").Return(int64(10)) - + blockStore.On("LoadBlockMeta", commonHeight).Return(&types.BlockMeta{Header: *common.Header}) + blockStore.On("LoadBlockMeta", height).Return(&types.BlockMeta{Header: *trusted.Header}) + blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit) + blockStore.On("LoadBlockCommit", height).Return(trusted.Commit) pool, err := evidence.NewPool(memdb.NewDB(), stateStore, blockStore) require.NoError(t, err) pool.SetLogger(log.TestingLogger()) evList := types.EvidenceList{ev} - err = pool.CheckEvidence(evList) - assert.NoError(t, err) + // check that the evidence pool correctly verifies the evidence + assert.NoError(t, pool.CheckEvidence(evList)) + // as it was not originally in the pending bucket, it should now have been added pendingEvs, _ := pool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes) - assert.Equal(t, 1, len(pendingEvs)) + require.Equal(t, 1, len(pendingEvs)) + assert.Equal(t, ev, pendingEvs[0]) // if we submit evidence only against a single byzantine validator when we see there are more validators then this // should return an error - ev.ByzantineValidators = []*types.Validator{commonVals.Validators[0]} - err = pool.CheckEvidence(evList) - assert.Error(t, err) - ev.ByzantineValidators = commonVals.Validators // restore evidence + ev.ByzantineValidators = ev.ByzantineValidators[:1] + t.Log(evList) + assert.Error(t, pool.CheckEvidence(evList)) + // restore original byz vals + ev.ByzantineValidators = ev.GetByzantineValidators(common.VoterSet, trusted.SignedHeader) + + // duplicate evidence should be rejected + evList = types.EvidenceList{ev, ev} + pool, err = evidence.NewPool(memdb.NewDB(), stateStore, blockStore) + require.NoError(t, err) + assert.Error(t, pool.CheckEvidence(evList)) // If evidence is submitted with an altered timestamp it should return an error ev.Timestamp = defaultEvidenceTime.Add(1 * time.Minute) - err = pool.CheckEvidence(evList) - assert.Error(t, err) + pool, err = evidence.NewPool(memdb.NewDB(), stateStore, blockStore) + require.NoError(t, err) + assert.Error(t, pool.AddEvidence(ev)) + ev.Timestamp = defaultEvidenceTime - evList = types.EvidenceList{forwardLunaticEv} - err = pool.CheckEvidence(evList) - assert.NoError(t, err) + // Evidence submitted with a different validator power should fail + ev.TotalVotingPower = 1 + pool, err = evidence.NewPool(memdb.NewDB(), stateStore, blockStore) + require.NoError(t, err) + assert.Error(t, pool.AddEvidence(ev)) + ev.TotalVotingPower = common.VoterSet.TotalVotingPower() +} + +func TestVerify_ForwardLunaticAttack(t *testing.T) { + const ( + nodeHeight int64 = 8 + attackHeight int64 = 10 + commonHeight int64 = 4 + totalVals = 10 + byzVals = 5 + ) + attackTime := defaultEvidenceTime.Add(1 * time.Hour) + + // create a forward lunatic attack + ev, trusted, common := makeLunaticEvidence( + t, attackHeight, commonHeight, totalVals, byzVals, totalVals-byzVals, defaultEvidenceTime, attackTime) + + // now we try to test verification against state + state := sm.State{ + LastBlockTime: defaultEvidenceTime.Add(2 * time.Hour), + LastBlockHeight: nodeHeight, + ConsensusParams: *types.DefaultConsensusParams(), + } + + // modify trusted light block so that it is of a height less than the conflicting one + trusted.Header.Height = state.LastBlockHeight + trusted.Header.Time = state.LastBlockTime + + stateStore := &smmocks.Store{} + stateStore.On("LoadValidators", commonHeight).Return(common.ValidatorSet, nil) + stateStore.On("LoadVoters", commonHeight, mock.AnythingOfType("*types.VoterParams")).Return( + ev.ConflictingBlock.VoterSet, nil) // Should use correct VoterSet for bls.VerifyAggregatedSignature + stateStore.On("Load").Return(state, nil) + blockStore := &mocks.BlockStore{} + blockStore.On("LoadBlockMeta", commonHeight).Return(&types.BlockMeta{Header: *common.Header}) + blockStore.On("LoadBlockMeta", nodeHeight).Return(&types.BlockMeta{Header: *trusted.Header}) + blockStore.On("LoadBlockMeta", attackHeight).Return(nil) + blockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit) + blockStore.On("LoadBlockCommit", nodeHeight).Return(trusted.Commit) + blockStore.On("Height").Return(nodeHeight) + pool, err := evidence.NewPool(memdb.NewDB(), stateStore, blockStore) + require.NoError(t, err) + + // check that the evidence pool correctly verifies the evidence + assert.NoError(t, pool.CheckEvidence(types.EvidenceList{ev})) + + // now we use a time which isn't able to contradict the FLA - thus we can't verify the evidence + oldBlockStore := &mocks.BlockStore{} + oldHeader := trusted.Header + oldHeader.Time = defaultEvidenceTime + oldBlockStore.On("LoadBlockMeta", commonHeight).Return(&types.BlockMeta{Header: *common.Header}) + oldBlockStore.On("LoadBlockMeta", nodeHeight).Return(&types.BlockMeta{Header: *oldHeader}) + oldBlockStore.On("LoadBlockMeta", attackHeight).Return(nil) + oldBlockStore.On("LoadBlockCommit", commonHeight).Return(common.Commit) + oldBlockStore.On("LoadBlockCommit", nodeHeight).Return(trusted.Commit) + oldBlockStore.On("Height").Return(nodeHeight) + require.Equal(t, defaultEvidenceTime, oldBlockStore.LoadBlockMeta(nodeHeight).Header.Time) + + pool, err = evidence.NewPool(memdb.NewDB(), stateStore, oldBlockStore) + require.NoError(t, err) + assert.Error(t, pool.CheckEvidence(types.EvidenceList{ev})) } func TestVerifyLightClientAttack_Equivocation(t *testing.T) { @@ -240,7 +235,7 @@ func TestVerifyLightClientAttack_Equivocation(t *testing.T) { VoterSet: conflictingVoters, }, CommonHeight: 10, - ByzantineValidators: conflictingVals.Validators[:4], + ByzantineValidators: conflictingVoters.Voters[:4], TotalVotingPower: 50, Timestamp: defaultEvidenceTime, } @@ -507,6 +502,108 @@ func TestVerifyDuplicateVoteEvidence(t *testing.T) { assert.Error(t, err) } +func makeLunaticEvidence( + t *testing.T, + height, commonHeight int64, + totalVals, byzVals, phantomVals int, + commonTime, attackTime time.Time, +) (ev *types.LightClientAttackEvidence, trusted *types.LightBlock, common *types.LightBlock) { + commonValSet, commonVoterSet, commonPrivVals := types.RandVoterSet(totalVals, defaultVotingPower) + require.Greater(t, totalVals, byzVals) + + // use the correct Proof to bypass the checks in libsodium + var proof []byte + proof, err := commonPrivVals[0].GenerateVRFProof([]byte{}) + require.NoError(t, err) + + // extract out the subset of byzantine validators in the common validator set + byzValidators, byzVoters, byzPrivVals := + commonValSet.Validators[:byzVals], commonVoterSet.Voters[:byzVals], commonPrivVals[:byzVals] + + phantomValSet, phantomVoterSet, phantomPrivVals := types.RandVoterSet(phantomVals, defaultVotingPower) + + conflictingVals := types.NewValidatorSet(append(phantomValSet.Validators, byzValidators...)) + conflictingVoters := types.ToVoterAll(append(phantomVoterSet.Voters, byzVoters...)) + conflictingPrivVals := append(phantomPrivVals, byzPrivVals...) + + // Should be the same order between VoterSet.Voters and PrivValidators for Commit + conflictingPrivVals = orderPrivValsByVoterSet(t, conflictingVoters, conflictingPrivVals) + + conflictingHeader := makeHeaderRandom(height) + conflictingHeader.Proof = proof + conflictingHeader.Time = attackTime + conflictingHeader.ValidatorsHash = conflictingVals.Hash() + conflictingHeader.VotersHash = conflictingVoters.Hash() + + blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) + voteSet := types.NewVoteSet(evidenceChainID, height, 1, tmproto.SignedMsgType(2), conflictingVoters) + commit, err := types.MakeCommit(blockID, height, 1, voteSet, conflictingPrivVals, defaultEvidenceTime) + require.NoError(t, err) + err = conflictingVoters.VerifyCommitLightTrusting(evidenceChainID, commit, light.DefaultTrustLevel) + require.NoError(t, err) + byzantineValidators := conflictingVoters.Copy().Voters + sort.Sort(types.ValidatorsByVotingPower(byzantineValidators)) + ev = &types.LightClientAttackEvidence{ + ConflictingBlock: &types.LightBlock{ + SignedHeader: &types.SignedHeader{ + Header: conflictingHeader, + Commit: commit, + }, + ValidatorSet: conflictingVals, + VoterSet: conflictingVoters, + }, + CommonHeight: commonHeight, + TotalVotingPower: commonVoterSet.TotalVotingPower(), + ByzantineValidators: byzantineValidators, + Timestamp: commonTime, + } + + trustedHeader := makeHeaderRandom(height) + trustedHeader.Proof = proof + trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) + trustedVals, trustedVoters, trustedPrivVals := types.RandVoterSet(totalVals, defaultVotingPower) + + // Should be the same order between VoterSet.Voters and PrivValidators for Commit + trustedPrivVals = orderPrivValsByVoterSet(t, trustedVoters, trustedPrivVals) + + trustedVoteSet := types.NewVoteSet(evidenceChainID, height, 1, tmproto.SignedMsgType(2), trustedVoters) + trustedCommit, err := types.MakeCommit(trustedBlockID, height, 1, trustedVoteSet, trustedPrivVals, defaultEvidenceTime) + require.NoError(t, err) + err = trustedVoters.VerifyCommitLightTrusting(evidenceChainID, trustedCommit, light.DefaultTrustLevel) + require.NoError(t, err) + trusted = &types.LightBlock{ + SignedHeader: &types.SignedHeader{ + Header: trustedHeader, + Commit: trustedCommit, + }, + ValidatorSet: trustedVals, + VoterSet: trustedVoters, + } + + commonHeader := makeHeaderRandom(commonHeight) + commonHeader.Proof = proof + commonHeader.Time = commonTime + common = &types.LightBlock{ + SignedHeader: &types.SignedHeader{ + Header: commonHeader, + // we can leave this empty because we shouldn't be checking this + Commit: &types.Commit{}, + }, + ValidatorSet: commonValSet, + VoterSet: commonVoterSet, + } + + return ev, trusted, common +} + +// func makeEquivocationEvidence() *types.LightClientAttackEvidence { + +// } + +// func makeAmnesiaEvidence() *types.LightClientAttackEvidence { + +// } + func makeVote( t *testing.T, val types.PrivValidator, chainID string, valIndex int32, height int64, round int32, step int, blockID types.BlockID, time time.Time) *types.Vote { @@ -567,3 +664,20 @@ func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.Bloc }, } } + +func orderPrivValsByVoterSet( + t *testing.T, voters *types.VoterSet, privVals []types.PrivValidator) []types.PrivValidator { + output := make([]types.PrivValidator, len(privVals)) + for idx, v := range voters.Voters { + for _, p := range privVals { + pubKey, err := p.GetPubKey() + require.NoError(t, err) + if bytes.Equal(v.Address, pubKey.Address()) { + output[idx] = p + break + } + } + require.NotEmpty(t, output[idx]) + } + return output +} diff --git a/test/e2e/tests/block_test.go b/test/e2e/tests/block_test.go index 170255332..6020e294e 100644 --- a/test/e2e/tests/block_test.go +++ b/test/e2e/tests/block_test.go @@ -37,8 +37,12 @@ func TestBlock_Header(t *testing.T) { } resp, err := client.Block(ctx, &block.Header.Height) require.NoError(t, err) + require.Equal(t, block, resp.Block, - "block mismatch for height %v", block.Header.Height) + "block mismatch for height %d", block.Header.Height) + + require.NoError(t, resp.Block.ValidateBasic(), + "block at height %d is invalid", block.Header.Height) } }) } diff --git a/types/evidence.go b/types/evidence.go index 23e6f5d46..020be3ebc 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -53,9 +53,9 @@ type DuplicateVoteEvidence struct { VoteB *Vote `json:"vote_b"` // abci specific information - TotalVotingPower int64 - VotingPower int64 - ValidatorPower int64 + TotalVotingPower int64 // VoterSet.totalVotingPower + VotingPower int64 // Validator.VotingPower + ValidatorPower int64 // Validator.StakingPower Timestamp time.Time } @@ -214,7 +214,7 @@ type LightClientAttackEvidence struct { // abci specific information ByzantineValidators []*Validator // validators in the validator set that misbehaved in creating the conflicting block - TotalVotingPower int64 // total voting power of the validator set at the common height + TotalVotingPower int64 // total voting power of the voter set at the common height Timestamp time.Time // timestamp of the block at the common height } @@ -251,20 +251,20 @@ func (l *LightClientAttackEvidence) Bytes() []byte { // GetByzantineValidators finds out what style of attack LightClientAttackEvidence was and then works out who // the malicious validators were and returns them. This is used both for forming the ByzantineValidators // field and for validating that it is correct. Validators are ordered based on validator power -func (l *LightClientAttackEvidence) GetByzantineValidators(commonVals *VoterSet, +func (l *LightClientAttackEvidence) GetByzantineValidators(commonVoters *VoterSet, trusted *SignedHeader) []*Validator { var validators []*Validator // First check if the header is invalid. This means that it is a lunatic attack and therefore we take the - // validators who are in the commonVals and voted for the lunatic header + // validators who are in the commonVoters and voted for the lunatic header if l.ConflictingHeaderIsInvalid(trusted.Header) { for _, commitSig := range l.ConflictingBlock.Commit.Signatures { if !commitSig.ForBlock() { continue } - _, val := commonVals.GetByAddress(commitSig.ValidatorAddress) + _, val := commonVoters.GetByAddress(commitSig.ValidatorAddress) if val == nil { - // validator wasn't in the common validator set + // validator wasn't in the common voter set continue } validators = append(validators, val) @@ -295,7 +295,7 @@ func (l *LightClientAttackEvidence) GetByzantineValidators(commonVals *VoterSet, } // if the rounds are different then this is an amnesia attack. Unfortunately, given the nature of the attack, // we aren't able yet to deduce which are malicious validators and which are not hence we return an - // empty validator set. + // empty voter set. return validators } @@ -315,7 +315,10 @@ func (l *LightClientAttackEvidence) ConflictingHeaderIsInvalid(trustedHeader *He // with evidence that have the same conflicting header and common height but different permutations // of validator commit signatures. The reason for this is that we don't want to allow several // permutations of the same evidence to be committed on chain. Ideally we commit the header with the -// most commit signatures (captures the most byzantine validators) but anything greater than 1/3 is sufficient. +// most commit signatures (captures the most byzantine validators) but anything greater than 1/3 is +// sufficient. +// TODO: We should change the hash to include the commit, header, total voting power, byzantine +// validators and timestamp func (l *LightClientAttackEvidence) Hash() []byte { buf := make([]byte, binary.MaxVarintLen64) n := binary.PutVarint(buf, l.CommonHeight) @@ -334,8 +337,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. @@ -354,8 +363,8 @@ 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 { @@ -370,6 +379,10 @@ func (l *LightClientAttackEvidence) ValidateBasic() error { 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 } @@ -441,6 +454,8 @@ func (evl EvidenceList) Hash() []byte { // the Evidence size is capped. evidenceBzs := make([][]byte, len(evl)) for i := 0; i < len(evl); i++ { + // TODO: We should change this to the hash. Using bytes contains some unexported data that + // may cause different hashes evidenceBzs[i] = evl[i].Bytes() } return merkle.HashFromByteSlices(evidenceBzs) diff --git a/types/evidence_test.go b/types/evidence_test.go index ef0bdf912..eaf66f0b1 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -196,9 +196,11 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { }) } -func TestLightClientAttackEvidence(t *testing.T) { +func TestLightClientAttackEvidenceBasic(t *testing.T) { height := int64(5) - voteSet, _, voterSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, 10, 1) + commonHeight := height - 1 + nValidators := 10 + voteSet, valSet, voterSet, 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"))) @@ -212,56 +214,52 @@ func TestLightClientAttackEvidence(t *testing.T) { }, VoterSet: voterSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: voterSet.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, - }, - VoterSet: voterSet, - }, - 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, - }, - VoterSet: voterSet, - }, - CommonHeight: height - 1, + 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 common height", func(ev *LightClientAttackEvidence) { + ev.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, + + for _, tc := range testCases { + lcae := &LightClientAttackEvidence{ + ConflictingBlock: &LightBlock{ + SignedHeader: &SignedHeader{ + Header: header, + Commit: commit, + }, + ValidatorSet: valSet, }, - VoterSet: voterSet, - }, - CommonHeight: height - 2, + CommonHeight: commonHeight, + TotalVotingPower: voterSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], + } + hash := lcae.Hash() + tc.malleateEvidence(lcae) + assert.NotEqual(t, hash, lcae.Hash(), tc.testName) } - assert.NotEqual(t, lcae.Hash(), differentEv.Hash()) - assert.Equal(t, lcae.Height(), int64(4)) // Height should be the common Height - assert.NotNil(t, lcae.Bytes()) } func TestLightClientAttackEvidenceValidation(t *testing.T) { height := int64(5) - voteSet, _, voterSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, 10, 1) + commonHeight := height - 1 + nValidators := 10 + voteSet, valSet, voterSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, nValidators, 1) header := makeHeaderRandom() header.Height = height header.VotersHash = voterSet.Hash() @@ -276,7 +274,10 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) { }, VoterSet: voterSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: voterSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], } assert.NoError(t, lcae.ValidateBasic()) @@ -285,16 +286,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.VoterSet = &VoterSet{} }, true}, + {"Negative total voting power", func(ev *LightClientAttackEvidence) { + ev.TotalVotingPower = -1 + }, true}, } for _, tc := range testCases { tc := tc @@ -307,7 +314,10 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) { }, VoterSet: voterSet, }, - CommonHeight: height - 1, + CommonHeight: commonHeight, + TotalVotingPower: voterSet.TotalVotingPower(), + Timestamp: header.Time, + ByzantineValidators: valSet.Validators[:nValidators/2], } tc.malleateEvidence(lcae) if tc.expectErr { diff --git a/types/light.go b/types/light.go index 9c89f624a..3d7d1986b 100644 --- a/types/light.go +++ b/types/light.go @@ -166,7 +166,7 @@ func (sh SignedHeader) ValidateBasic(chainID string) error { if sh.Commit.Height != sh.Height { return fmt.Errorf("header and commit height mismatch: %d vs %d", sh.Height, sh.Commit.Height) } - if hhash, chash := sh.Hash(), sh.Commit.BlockID.Hash; !bytes.Equal(hhash, chash) { + if hhash, chash := sh.Header.Hash(), sh.Commit.BlockID.Hash; !bytes.Equal(hhash, chash) { return fmt.Errorf("commit signs block %X, header is block %X", chash, hhash) } diff --git a/types/light_test.go b/types/light_test.go index 7f668831b..44af0373d 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, ) }) } diff --git a/types/validator.go b/types/validator.go index e008b1ff4..92a083dfe 100644 --- a/types/validator.go +++ b/types/validator.go @@ -28,7 +28,7 @@ type Validator struct { ProposerPriority int64 `json:"proposer_priority"` } -// NewValidator returns a new validator with the given pubkey and voting power. +// NewValidator returns a new validator with the given pubkey and staking power. func NewValidator(pubKey crypto.PubKey, stakingPower int64) *Validator { return &Validator{ Address: pubKey.Address(),