You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

443 lines
15 KiB

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"
abci "github.com/tendermint/tendermint/abci/types"
"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)
func TestEvidencePoolBasic(t *testing.T) {
var (
height = int64(1)
stateStore = &smmocks.Store{}
evidenceDB = dbm.NewMemDB()
blockStore = &mocks.BlockStore{}
)
valSet, privVals := types.RandValidatorSet(3, 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)
// evidence not seen yet:
evs := pool.PendingEvidence(10)
assert.Equal(t, 0, len(evs))
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))
evs = pool.PendingEvidence(10)
assert.Equal(t, 1, len(evs))
// shouldn't be able to add evidence twice
assert.Error(t, pool.AddEvidence(ev))
assert.Equal(t, 1, len(pool.PendingEvidence(10)))
}
// 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.Add(time.Duration(height) * time.Minute)}}
}
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"},
}
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 TestAddEvidenceFromConsensus(t *testing.T) {
var height int64 = 10
pool, val := defaultTestPool(height)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, val, evidenceChainID)
err := pool.AddEvidenceFromConsensus(ev, defaultEvidenceTime,
types.NewValidatorSet([]*types.Validator{val.ExtractIntoValidator(2)}))
assert.NoError(t, err)
next := pool.EvidenceFront()
assert.Equal(t, ev, next.Value.(types.Evidence))
// shouldn't be able to submit the same evidence twice
err = pool.AddEvidenceFromConsensus(ev, defaultEvidenceTime.Add(-1*time.Second),
types.NewValidatorSet([]*types.Validator{val.ExtractIntoValidator(3)}))
if assert.Error(t, err) {
assert.Equal(t, "evidence already verified and added", err.Error())
}
}
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,
val, evidenceChainID)
err := pool.AddEvidence(prunedEv)
require.NoError(t, err)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, 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)
byzVals := pool.ABCIEvidence(block.Height, block.Evidence.Evidence)
expectedByzVals := []abci.Evidence{
{
Type: abci.EvidenceType_DUPLICATE_VOTE,
Validator: types.TM2PB.Validator(val.ExtractIntoValidator(10)),
Height: height,
Time: defaultEvidenceTime.Add(time.Duration(height) * time.Minute),
TotalVotingPower: 10,
},
}
assert.Equal(t, expectedByzVals, byzVals)
assert.Equal(t, 1, len(pool.PendingEvidence(10)))
pool.Update(state)
// a) Update marks evidence as committed so pending evidence should be empty
assert.Empty(t, pool.PendingEvidence(10))
// 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())
}
assert.Empty(t, pool.ABCIEvidence(height, []types.Evidence{}))
}
func TestVerifyPendingEvidencePasses(t *testing.T) {
var height int64 = 1
pool, val := defaultTestPool(height)
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, 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, 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
func TestCheckEvidenceWithLightClientAttack(t *testing.T) {
nValidators := 5
conflictingVals, conflictingPrivVals := types.RandValidatorSet(nValidators, 10)
trustedHeader := makeHeaderRandom(10)
conflictingHeader := makeHeaderRandom(10)
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, 10, 1, tmproto.SignedMsgType(2), conflictingVals)
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,
},
CommonHeight: 10,
}
trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash"))
trustedVoteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVals)
trustedCommit, err := types.MakeCommit(trustedBlockID, 10, 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", int64(10)).Return(conflictingVals, nil)
stateStore.On("Load").Return(state, nil)
blockStore := &mocks.BlockStore{}
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader})
blockStore.On("LoadBlockCommit", int64(10)).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 we move to full verification where the evidence should still pass
commit.Signatures = append(commit.Signatures[:nValidators-1], types.NewCommitSigAbsent())
err = pool.CheckEvidence(types.EvidenceList{ev})
assert.NoError(t, err)
// take away the last two signatures -> should fail due to insufficient power
commit.Signatures = append(commit.Signatures[:nValidators-2], types.NewCommitSigAbsent(), types.NewCommitSigAbsent())
err = pool.CheckEvidence(types.EvidenceList{ev})
assert.Error(t, err)
}
func TestRecoverPendingEvidence(t *testing.T) {
height := int64(10)
expiredEvidenceTime := time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC)
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)
pool, err := evidence.NewPool(evidenceDB, stateStore, blockStore)
require.NoError(t, err)
pool.SetLogger(log.TestingLogger())
goodEvidence := types.NewMockDuplicateVoteEvidenceWithValidator(height,
defaultEvidenceTime, val, evidenceChainID)
expiredEvidence := types.NewMockDuplicateVoteEvidenceWithValidator(int64(1),
expiredEvidenceTime, val, evidenceChainID)
err = pool.AddEvidence(goodEvidence)
require.NoError(t, err)
err = pool.AddEvidence(expiredEvidence)
require.NoError(t, err)
newStateStore := &smmocks.Store{}
newStateStore.On("Load").Return(sm.State{
LastBlockTime: defaultEvidenceTime.Add(49 * time.Hour),
LastBlockHeight: height + 12,
ConsensusParams: tmproto.ConsensusParams{
Block: tmproto.BlockParams{
MaxBytes: 22020096,
MaxGas: -1,
},
Evidence: tmproto.EvidenceParams{
MaxAgeNumBlocks: 20,
MaxAgeDuration: 1 * time.Hour,
MaxNum: 50,
},
},
}, nil)
newPool, err := evidence.NewPool(evidenceDB, newStateStore, blockStore)
assert.NoError(t, err)
assert.Equal(t, 1, len(newPool.PendingEvidence(10)))
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,
MaxNum: 50,
},
},
}
// 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(),
}
}