Files
tendermint/evidence/pool_test.go
Callum a2a6852ab9 use correct source of evidence time
Conflicting votes are now sent to the evidence pool to form duplicate vote evidence only once
the height of the evidence is finished and the time of the block finalised.
2021-01-19 16:00:02 +01:00

466 lines
16 KiB
Go

package evidence_test
import (
"os"
"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/evidence"
"github.com/tendermint/tendermint/evidence/mocks"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmversion "github.com/tendermint/tendermint/proto/tendermint/version"
sm "github.com/tendermint/tendermint/state"
smmocks "github.com/tendermint/tendermint/state/mocks"
"github.com/tendermint/tendermint/store"
"github.com/tendermint/tendermint/types"
"github.com/tendermint/tendermint/version"
)
func TestMain(m *testing.M) {
code := m.Run()
os.Exit(code)
}
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{}
)
valSet, privVals := types.RandValidatorSet(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(evidenceDB, stateStore, blockStore)
require.NoError(t, err)
pool.SetLogger(log.TestingLogger())
// evidence not seen yet:
evs, size := pool.PendingEvidence(defaultEvidenceMaxBytes)
assert.Equal(t, 0, len(evs))
assert.Zero(t, size)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, privVals[0], evidenceChainID)
// good evidence
evAdded := make(chan struct{})
go func() {
<-pool.EvidenceWaitChan()
close(evAdded)
}()
// evidence seen but not yet committed:
assert.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()
assert.Equal(t, ev, next.Value.(types.Evidence))
const evidenceBytes int64 = 372
evs, size = pool.PendingEvidence(evidenceBytes)
assert.Equal(t, 1, len(evs))
assert.Equal(t, evidenceBytes, size) // check that the size of the single evidence in bytes is correct
// shouldn't be able to add evidence twice
assert.NoError(t, pool.AddEvidence(ev))
evs, _ = pool.PendingEvidence(defaultEvidenceMaxBytes)
assert.Equal(t, 1, len(evs))
}
// Tests inbound evidence for the right time and height
func TestAddExpiredEvidence(t *testing.T) {
var (
val = types.NewMockPV()
height = int64(30)
stateStore = initializeValidatorState(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(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) {
ev := types.NewMockDuplicateVoteEvidenceWithValidator(tc.evHeight, tc.evTime, val, evidenceChainID)
err := pool.AddEvidence(ev)
if tc.expErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestReportConflictingVotes(t *testing.T) {
var height int64 = 10
pool, pv := defaultTestPool(height)
val := types.NewValidator(pv.PrivKey.PubKey(), 10)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height+1, defaultEvidenceTime, pv, evidenceChainID)
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)
}
func TestEvidencePoolUpdate(t *testing.T) {
height := int64(21)
pool, val := defaultTestPool(height)
state := pool.State()
// create new block (no need to save it to blockStore)
prunedEv := types.NewMockDuplicateVoteEvidenceWithValidator(1, defaultEvidenceTime.Add(1*time.Minute),
val, evidenceChainID)
err := pool.AddEvidence(prunedEv)
require.NoError(t, err)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime.Add(21*time.Minute),
val, evidenceChainID)
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)
err = pool.CheckEvidence(types.EvidenceList{ev})
require.NoError(t, err)
pool.Update(state, block.Evidence.Evidence)
// a) Update marks evidence as committed so pending evidence should be empty
evList, evSize := pool.PendingEvidence(defaultEvidenceMaxBytes)
assert.Empty(t, evList)
assert.Zero(t, evSize)
// 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
pool, val := defaultTestPool(height)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime.Add(1*time.Minute),
val, evidenceChainID)
err := pool.AddEvidence(ev)
require.NoError(t, err)
err = pool.CheckEvidence(types.EvidenceList{ev})
assert.NoError(t, err)
}
func TestVerifyDuplicatedEvidenceFails(t *testing.T) {
var height int64 = 1
pool, val := defaultTestPool(height)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime.Add(1*time.Minute),
val, evidenceChainID)
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 TestCheckEvidenceWithLightClientAttack(t *testing.T) {
var (
nValidators = 5
validatorPower int64 = 10
height int64 = 10
)
conflictingVals, conflictingPrivVals := types.RandValidatorSet(nValidators, validatorPower)
trustedHeader := makeHeaderRandom(height)
trustedHeader.Time = defaultEvidenceTime
conflictingHeader := makeHeaderRandom(height)
conflictingHeader.ValidatorsHash = conflictingVals.Hash()
trustedHeader.ValidatorsHash = conflictingHeader.ValidatorsHash
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), conflictingVals)
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,
},
CommonHeight: 10,
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), conflictingVals)
trustedCommit, err := types.MakeCommit(trustedBlockID, height, 1, trustedVoteSet, conflictingPrivVals,
defaultEvidenceTime)
require.NoError(t, err)
state := sm.State{
LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute),
LastBlockHeight: 11,
ConsensusParams: *types.DefaultConsensusParams(),
}
stateStore := &smmocks.Store{}
stateStore.On("LoadValidators", height).Return(conflictingVals, nil)
stateStore.On("Load").Return(state, nil)
blockStore := &mocks.BlockStore{}
blockStore.On("LoadBlockMeta", height).Return(&types.BlockMeta{Header: *trustedHeader})
blockStore.On("LoadBlockCommit", height).Return(trustedCommit)
pool, err := evidence.NewPool(dbm.NewMemDB(), stateStore, blockStore)
require.NoError(t, err)
pool.SetLogger(log.TestingLogger())
err = pool.AddEvidence(ev)
assert.NoError(t, err)
err = pool.CheckEvidence(types.EvidenceList{ev})
assert.NoError(t, err)
// 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())
err = pool.CheckEvidence(types.EvidenceList{ev})
assert.Error(t, err)
}
// 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) {
height := int64(10)
val := types.NewMockPV()
valAddress := val.PrivKey.PubKey().Address()
evidenceDB := dbm.NewMemDB()
stateStore := initializeValidatorState(val, height)
state, err := stateStore.Load()
require.NoError(t, err)
blockStore := initializeBlockStore(dbm.NewMemDB(), state, valAddress)
// create previous pool and populate it
pool, err := evidence.NewPool(evidenceDB, stateStore, blockStore)
require.NoError(t, err)
pool.SetLogger(log.TestingLogger())
goodEvidence := types.NewMockDuplicateVoteEvidenceWithValidator(height,
defaultEvidenceTime.Add(10*time.Minute), val, evidenceChainID)
expiredEvidence := types.NewMockDuplicateVoteEvidenceWithValidator(int64(1),
defaultEvidenceTime.Add(1*time.Minute), val, evidenceChainID)
err = pool.AddEvidence(goodEvidence)
require.NoError(t, err)
err = pool.AddEvidence(expiredEvidence)
require.NoError(t, err)
// 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: tmproto.ConsensusParams{
Block: tmproto.BlockParams{
MaxBytes: 22020096,
MaxGas: -1,
},
Evidence: tmproto.EvidenceParams{
MaxAgeNumBlocks: 20,
MaxAgeDuration: 20 * time.Minute,
MaxBytes: 1000,
},
},
}, nil)
newPool, err := evidence.NewPool(evidenceDB, newStateStore, blockStore)
assert.NoError(t, err)
evList, _ := newPool.PendingEvidence(defaultEvidenceMaxBytes)
assert.Equal(t, 1, len(evList))
next := newPool.EvidenceFront()
assert.Equal(t, goodEvidence, next.Value.(types.Evidence))
}
func initializeStateFromValidatorSet(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: tmproto.ConsensusParams{
Block: tmproto.BlockParams{
MaxBytes: 22020096,
MaxGas: -1,
},
Evidence: tmproto.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
if err := stateStore.Save(state); err != nil {
panic(err)
}
}
return stateStore
}
func initializeValidatorState(privVal types.PrivValidator, height int64) sm.Store {
pubKey, _ := privVal.GetPubKey()
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(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 {
blockStore := store.NewBlockStore(db)
for i := int64(1); i <= state.LastBlockHeight; i++ {
lastCommit := makeCommit(i-1, valAddr)
block, _ := state.MakeBlock(i, []types.Tx{}, lastCommit, nil,
state.Validators.GetProposer().Address)
block.Header.Time = defaultEvidenceTime.Add(time.Duration(i) * time.Minute)
block.Header.Version = tmversion.Consensus{Block: version.BlockProtocol, App: 1}
const parts = 1
partSet := block.MakePartSet(parts)
seenCommit := makeCommit(i, valAddr)
blockStore.SaveBlock(block, partSet, seenCommit)
}
return blockStore
}
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(height int64) (*evidence.Pool, types.MockPV) {
val := types.NewMockPV()
valAddress := val.PrivKey.PubKey().Address()
evidenceDB := dbm.NewMemDB()
stateStore := initializeValidatorState(val, height)
state, _ := stateStore.Load()
blockStore := initializeBlockStore(dbm.NewMemDB(), state, valAddress)
pool, err := evidence.NewPool(evidenceDB, stateStore, blockStore)
if err != nil {
panic("test evidence pool could not be created")
}
pool.SetLogger(log.TestingLogger())
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(),
}
}