package evidence_test import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/internal/evidence" "github.com/tendermint/tendermint/internal/evidence/mocks" sm "github.com/tendermint/tendermint/internal/state" smmocks "github.com/tendermint/tendermint/internal/state/mocks" sf "github.com/tendermint/tendermint/internal/state/test/factory" "github.com/tendermint/tendermint/internal/store" "github.com/tendermint/tendermint/internal/test/factory" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/version" ) const evidenceChainID = "test_chain" var ( defaultEvidenceTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) defaultEvidenceMaxBytes int64 = 1000 ) func TestEvidencePoolBasic(t *testing.T) { var ( height = int64(1) stateStore = &smmocks.Store{} evidenceDB = dbm.NewMemDB() blockStore = &mocks.BlockStore{} ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() valSet, privVals := factory.ValidatorSet(ctx, t, 1, 10) blockStore.On("LoadBlockMeta", mock.AnythingOfType("int64")).Return( &types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}}, ) stateStore.On("LoadValidators", mock.AnythingOfType("int64")).Return(valSet, nil) stateStore.On("Load").Return(createState(height+1, valSet), nil) pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore) require.NoError(t, err) // evidence not seen yet: evs, size := pool.PendingEvidence(defaultEvidenceMaxBytes) require.Equal(t, 0, len(evs)) require.Zero(t, size) ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(ctx, height, defaultEvidenceTime, privVals[0], evidenceChainID) require.NoError(t, err) // good evidence evAdded := make(chan struct{}) go func() { <-pool.EvidenceWaitChan() close(evAdded) }() // evidence seen but not yet committed: require.NoError(t, pool.AddEvidence(ev)) select { case <-evAdded: case <-time.After(5 * time.Second): t.Fatal("evidence was not added to list after 5s") } next := pool.EvidenceFront() require.Equal(t, ev, next.Value.(types.Evidence)) const evidenceBytes int64 = 372 evs, size = pool.PendingEvidence(evidenceBytes) require.Equal(t, 1, len(evs)) require.Equal(t, evidenceBytes, size) // check that the size of the single evidence in bytes is correct // shouldn't be able to add evidence twice require.NoError(t, pool.AddEvidence(ev)) evs, _ = pool.PendingEvidence(defaultEvidenceMaxBytes) require.Equal(t, 1, len(evs)) } // Tests inbound evidence for the right time and height func TestAddExpiredEvidence(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var ( val = types.NewMockPV() height = int64(30) stateStore = initializeValidatorState(ctx, t, val, height) evidenceDB = dbm.NewMemDB() blockStore = &mocks.BlockStore{} expiredEvidenceTime = time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC) expiredHeight = int64(2) ) blockStore.On("LoadBlockMeta", mock.AnythingOfType("int64")).Return(func(h int64) *types.BlockMeta { if h == height || h == expiredHeight { return &types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}} } return &types.BlockMeta{Header: types.Header{Time: expiredEvidenceTime}} }) pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore) require.NoError(t, err) testCases := []struct { evHeight int64 evTime time.Time expErr bool evDescription string }{ {height, defaultEvidenceTime, false, "valid evidence"}, {expiredHeight, defaultEvidenceTime, false, "valid evidence (despite old height)"}, {height - 1, expiredEvidenceTime, false, "valid evidence (despite old time)"}, {expiredHeight - 1, expiredEvidenceTime, true, "evidence from height 1 (created at: 2019-01-01 00:00:00 +0000 UTC) is too old"}, {height, defaultEvidenceTime.Add(1 * time.Minute), true, "evidence time and block time is different"}, } for _, tc := range testCases { tc := tc t.Run(tc.evDescription, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(ctx, tc.evHeight, tc.evTime, val, evidenceChainID) require.NoError(t, err) err = pool.AddEvidence(ev) if tc.expErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestReportConflictingVotes(t *testing.T) { var height int64 = 10 ctx, cancel := context.WithCancel(context.Background()) defer cancel() pool, pv := defaultTestPool(ctx, t, height) val := types.NewValidator(pv.PrivKey.PubKey(), 10) ev, err := types.NewMockDuplicateVoteEvidenceWithValidator(ctx, height+1, defaultEvidenceTime, pv, evidenceChainID) require.NoError(t, err) pool.ReportConflictingVotes(ev.VoteA, ev.VoteB) // shouldn't be able to submit the same evidence twice pool.ReportConflictingVotes(ev.VoteA, ev.VoteB) // evidence from consensus should not be added immediately but reside in the consensus buffer evList, evSize := pool.PendingEvidence(defaultEvidenceMaxBytes) require.Empty(t, evList) require.Zero(t, evSize) next := pool.EvidenceFront() require.Nil(t, next) // move to next height and update state and evidence pool state := pool.State() state.LastBlockHeight++ state.LastBlockTime = ev.Time() state.LastValidators = types.NewValidatorSet([]*types.Validator{val}) pool.Update(state, []types.Evidence{}) // 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) { height := int64(21) ctx, cancel := context.WithCancel(context.Background()) defer cancel() pool, val := defaultTestPool(ctx, t, height) state := pool.State() // create two lots of old evidence that we expect to be pruned when we update prunedEv, err := types.NewMockDuplicateVoteEvidenceWithValidator(ctx, 1, defaultEvidenceTime.Add(1*time.Minute), val, evidenceChainID, ) require.NoError(t, err) notPrunedEv, err := types.NewMockDuplicateVoteEvidenceWithValidator(ctx, 2, defaultEvidenceTime.Add(2*time.Minute), val, evidenceChainID, ) require.NoError(t, err) require.NoError(t, pool.AddEvidence(prunedEv)) require.NoError(t, pool.AddEvidence(notPrunedEv)) ev, err := types.NewMockDuplicateVoteEvidenceWithValidator( ctx, height, defaultEvidenceTime.Add(21*time.Minute), val, evidenceChainID, ) require.NoError(t, err) lastCommit := makeCommit(height, val.PrivKey.PubKey().Address()) block := types.MakeBlock(height+1, []types.Tx{}, lastCommit, []types.Evidence{ev}) // update state (partially) state.LastBlockHeight = height + 1 state.LastBlockTime = defaultEvidenceTime.Add(22 * time.Minute) evList, _ := pool.PendingEvidence(2 * defaultEvidenceMaxBytes) require.Equal(t, 2, len(evList)) require.Equal(t, uint32(2), pool.Size()) require.NoError(t, pool.CheckEvidence(types.EvidenceList{ev})) evList, _ = pool.PendingEvidence(3 * defaultEvidenceMaxBytes) require.Equal(t, 3, len(evList)) require.Equal(t, uint32(3), pool.Size()) pool.Update(state, block.Evidence) // a) Update marks evidence as committed so pending evidence should be empty evList, _ = pool.PendingEvidence(defaultEvidenceMaxBytes) require.Equal(t, []types.Evidence{notPrunedEv}, evList) // b) If we try to check this evidence again it should fail because it has already been committed err = pool.CheckEvidence(types.EvidenceList{ev}) if assert.Error(t, err) { assert.Equal(t, "evidence was already committed", err.(*types.ErrInvalidEvidence).Reason.Error()) } } func TestVerifyPendingEvidencePasses(t *testing.T) { var height int64 = 1 ctx, cancel := context.WithCancel(context.Background()) defer cancel() pool, val := defaultTestPool(ctx, t, height) ev, err := types.NewMockDuplicateVoteEvidenceWithValidator( ctx, height, defaultEvidenceTime.Add(1*time.Minute), val, evidenceChainID, ) require.NoError(t, err) require.NoError(t, pool.AddEvidence(ev)) require.NoError(t, pool.CheckEvidence(types.EvidenceList{ev})) } func TestVerifyDuplicatedEvidenceFails(t *testing.T) { var height int64 = 1 ctx, cancel := context.WithCancel(context.Background()) defer cancel() pool, val := defaultTestPool(ctx, t, height) ev, err := types.NewMockDuplicateVoteEvidenceWithValidator( ctx, height, defaultEvidenceTime.Add(1*time.Minute), val, evidenceChainID, ) require.NoError(t, err) err = pool.CheckEvidence(types.EvidenceList{ev, ev}) if assert.Error(t, err) { assert.Equal(t, "duplicate evidence", err.(*types.ErrInvalidEvidence).Reason.Error()) } } // check that valid light client evidence is correctly validated and stored in // evidence pool func TestLightClientAttackEvidenceLifecycle(t *testing.T) { var ( height int64 = 100 commonHeight int64 = 90 ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() ev, trusted, common := makeLunaticEvidence(ctx, t, height, commonHeight, 10, 5, 5, defaultEvidenceTime, defaultEvidenceTime.Add(1*time.Hour)) state := sm.State{ LastBlockTime: defaultEvidenceTime.Add(2 * time.Hour), LastBlockHeight: 110, ConsensusParams: *types.DefaultConsensusParams(), } stateStore := &smmocks.Store{} stateStore.On("LoadValidators", height).Return(trusted.ValidatorSet, nil) stateStore.On("LoadValidators", commonHeight).Return(common.ValidatorSet, nil) stateStore.On("Load").Return(state, nil) blockStore := &mocks.BlockStore{} 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(log.TestingLogger(), dbm.NewMemDB(), stateStore, blockStore) require.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 // pending evidence and continue to gossip it func TestRecoverPendingEvidence(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() height := int64(10) val := types.NewMockPV() valAddress := val.PrivKey.PubKey().Address() evidenceDB := dbm.NewMemDB() stateStore := initializeValidatorState(ctx, t, val, height) state, err := stateStore.Load() require.NoError(t, err) blockStore, err := initializeBlockStore(dbm.NewMemDB(), state, valAddress) require.NoError(t, err) // create previous pool and populate it pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore) require.NoError(t, err) goodEvidence, err := types.NewMockDuplicateVoteEvidenceWithValidator( ctx, height, defaultEvidenceTime.Add(10*time.Minute), val, evidenceChainID, ) require.NoError(t, err) expiredEvidence, err := types.NewMockDuplicateVoteEvidenceWithValidator( ctx, int64(1), defaultEvidenceTime.Add(1*time.Minute), val, evidenceChainID, ) require.NoError(t, err) require.NoError(t, pool.AddEvidence(goodEvidence)) require.NoError(t, pool.AddEvidence(expiredEvidence)) // now recover from the previous pool at a different time newStateStore := &smmocks.Store{} newStateStore.On("Load").Return(sm.State{ LastBlockTime: defaultEvidenceTime.Add(25 * time.Minute), LastBlockHeight: height + 15, ConsensusParams: types.ConsensusParams{ Block: types.BlockParams{ MaxBytes: 22020096, MaxGas: -1, }, Evidence: types.EvidenceParams{ MaxAgeNumBlocks: 20, MaxAgeDuration: 20 * time.Minute, MaxBytes: defaultEvidenceMaxBytes, }, }, }, nil) newPool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, newStateStore, blockStore) require.NoError(t, err) evList, _ := newPool.PendingEvidence(defaultEvidenceMaxBytes) require.Equal(t, 1, len(evList)) next := newPool.EvidenceFront() require.Equal(t, goodEvidence, next.Value.(types.Evidence)) } func initializeStateFromValidatorSet(t *testing.T, valSet *types.ValidatorSet, height int64) sm.Store { stateDB := dbm.NewMemDB() stateStore := sm.NewStore(stateDB) state := sm.State{ ChainID: evidenceChainID, InitialHeight: 1, LastBlockHeight: height, LastBlockTime: defaultEvidenceTime, Validators: valSet, NextValidators: valSet.CopyIncrementProposerPriority(1), LastValidators: valSet, LastHeightValidatorsChanged: 1, ConsensusParams: types.ConsensusParams{ Block: types.BlockParams{ MaxBytes: 22020096, MaxGas: -1, }, Evidence: types.EvidenceParams{ MaxAgeNumBlocks: 20, MaxAgeDuration: 20 * time.Minute, MaxBytes: 1000, }, }, } // save all states up to height for i := int64(0); i <= height; i++ { state.LastBlockHeight = i require.NoError(t, stateStore.Save(state)) } return stateStore } func initializeValidatorState(ctx context.Context, t *testing.T, privVal types.PrivValidator, height int64) sm.Store { pubKey, _ := privVal.GetPubKey(ctx) validator := &types.Validator{Address: pubKey.Address(), VotingPower: 10, PubKey: pubKey} // create validator set and state valSet := &types.ValidatorSet{ Validators: []*types.Validator{validator}, Proposer: validator, } return initializeStateFromValidatorSet(t, valSet, height) } // initializeBlockStore creates a block storage and populates it w/ a dummy // block at +height+. func initializeBlockStore(db dbm.DB, state sm.State, valAddr []byte) (*store.BlockStore, error) { blockStore := store.NewBlockStore(db) for i := int64(1); i <= state.LastBlockHeight; i++ { lastCommit := makeCommit(i-1, valAddr) block, err := sf.MakeBlock(state, i, lastCommit) if err != nil { return nil, err } block.Header.Time = defaultEvidenceTime.Add(time.Duration(i) * time.Minute) block.Header.Version = version.Consensus{Block: version.BlockProtocol, App: 1} const parts = 1 partSet, err := block.MakePartSet(parts) if err != nil { return nil, err } seenCommit := makeCommit(i, valAddr) blockStore.SaveBlock(block, partSet, seenCommit) } return blockStore, nil } func makeCommit(height int64, valAddr []byte) *types.Commit { commitSigs := []types.CommitSig{{ BlockIDFlag: types.BlockIDFlagCommit, ValidatorAddress: valAddr, Timestamp: defaultEvidenceTime, Signature: []byte("Signature"), }} return types.NewCommit(height, 0, types.BlockID{}, commitSigs) } func defaultTestPool(ctx context.Context, t *testing.T, height int64) (*evidence.Pool, types.MockPV) { t.Helper() val := types.NewMockPV() valAddress := val.PrivKey.PubKey().Address() evidenceDB := dbm.NewMemDB() stateStore := initializeValidatorState(ctx, t, val, height) state, err := stateStore.Load() require.NoError(t, err) blockStore, err := initializeBlockStore(dbm.NewMemDB(), state, valAddress) require.NoError(t, err) pool, err := evidence.NewPool(log.TestingLogger(), evidenceDB, stateStore, blockStore) require.NoError(t, err, "test evidence pool could not be created") return pool, val } func createState(height int64, valSet *types.ValidatorSet) sm.State { return sm.State{ ChainID: evidenceChainID, LastBlockHeight: height, LastBlockTime: defaultEvidenceTime, Validators: valSet, ConsensusParams: *types.DefaultConsensusParams(), } }