Files
tendermint/internal/evidence/pool_test.go
William Banfield 0aa3b0b6fc Proposer-Based Timestamps Merge (#7605)
This pull request merges in the changes for implementing Proposer-based timestamps into `master`. The power was primarily being done in the `wb/proposer-based-timestamps` branch, with changes being merged into that branch during development. This pull request represents an amalgamation of the changes made into that development branch. All of the changes that were placed into that branch have been cleanly rebased on top of the latest `master`. The changes compile and the tests pass insofar as our tests in general pass.

### Note To Reviewers
 These changes have been extensively reviewed during development. There is not much new here. In the interest of making effective use of time, I would recommend against trying to perform a complete audit of the changes presented and instead examine for mistakes that may have occurred during the process of rebasing the changes. I gave the complete change set a first pass for any issues, but additional eyes would be very appreciated. 

In sum, this change set does the following:
closes #6942 
merges in #6849
2022-01-26 16:00:23 +00:00

541 lines
16 KiB
Go

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.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(),
}
}