mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-09 21:40:11 +00:00
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
541 lines
16 KiB
Go
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(),
|
|
}
|
|
}
|