package state_test import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" dbm "git.cw.tr/mukan-network/mukan-consensus-db" abci "git.cw.tr/mukan-network/mukan-consensus/abci/types" "git.cw.tr/mukan-network/mukan-consensus/crypto/ed25519" "git.cw.tr/mukan-network/mukan-consensus/crypto/tmhash" "git.cw.tr/mukan-network/mukan-consensus/internal/test" "git.cw.tr/mukan-network/mukan-consensus/libs/log" mpmocks "git.cw.tr/mukan-network/mukan-consensus/mempool/mocks" cmtproto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/types" sm "git.cw.tr/mukan-network/mukan-consensus/state" "git.cw.tr/mukan-network/mukan-consensus/state/mocks" "git.cw.tr/mukan-network/mukan-consensus/store" "git.cw.tr/mukan-network/mukan-consensus/types" cmttime "git.cw.tr/mukan-network/mukan-consensus/types/time" ) const validationTestsStopHeight int64 = 10 func TestValidateBlockHeader(t *testing.T) { proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() //nolint:errcheck // ignore for tests state, stateDB, privVals := makeState(3, 1) stateStore := sm.NewStore(stateDB, sm.StoreOptions{ DiscardABCIResponses: false, }) mp := &mpmocks.Mempool{} mp.On("Lock").Return() mp.On("Unlock").Return() mp.On("FlushAppConn", mock.Anything).Return(nil) mp.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) blockStore := store.NewBlockStore(dbm.NewMemDB()) blockExec := sm.NewBlockExecutor( stateStore, log.TestingLogger(), proxyApp.Consensus(), mp, sm.EmptyEvidencePool{}, blockStore, ) lastCommit := &types.Commit{} var lastExtCommit *types.ExtendedCommit // some bad values wrongHash := tmhash.Sum([]byte("this hash is wrong")) wrongVersion1 := state.Version.Consensus wrongVersion1.Block += 2 wrongVersion2 := state.Version.Consensus wrongVersion2.App += 2 // Manipulation of any header field causes failure. testCases := []struct { name string malleateBlock func(block *types.Block) }{ {"Version wrong1", func(block *types.Block) { block.Version = wrongVersion1 }}, {"Version wrong2", func(block *types.Block) { block.Version = wrongVersion2 }}, {"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }}, {"Height wrong", func(block *types.Block) { block.Height += 10 }}, {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }}, {"LastBlockID wrong", func(block *types.Block) { block.LastBlockID.PartSetHeader.Total += 10 }}, {"LastCommitHash wrong", func(block *types.Block) { block.LastCommitHash = wrongHash }}, {"DataHash wrong", func(block *types.Block) { block.DataHash = wrongHash }}, {"ValidatorsHash wrong", func(block *types.Block) { block.ValidatorsHash = wrongHash }}, {"NextValidatorsHash wrong", func(block *types.Block) { block.NextValidatorsHash = wrongHash }}, {"ConsensusHash wrong", func(block *types.Block) { block.ConsensusHash = wrongHash }}, {"AppHash wrong", func(block *types.Block) { block.AppHash = wrongHash }}, {"LastResultsHash wrong", func(block *types.Block) { block.LastResultsHash = wrongHash }}, {"EvidenceHash wrong", func(block *types.Block) { block.EvidenceHash = wrongHash }}, {"Proposer wrong", func(block *types.Block) { block.ProposerAddress = ed25519.GenPrivKey().PubKey().Address() }}, {"Proposer invalid", func(block *types.Block) { block.ProposerAddress = []byte("wrong size") }}, } // Build up state for multiple heights for height := int64(1); height < validationTestsStopHeight; height++ { /* Invalid blocks don't pass */ for _, tc := range testCases { block, err := makeBlock(state, height, lastCommit) require.NoError(t, err) tc.malleateBlock(block) err = blockExec.ValidateBlock(state, block) require.Error(t, err, tc.name) } /* A good block passes */ var err error state, _, lastExtCommit, err = makeAndCommitGoodBlock( state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil) require.NoError(t, err, "height %d", height) lastCommit = lastExtCommit.ToCommit() } nextHeight := validationTestsStopHeight block, err := makeBlock(state, nextHeight, lastCommit) require.NoError(t, err) state.InitialHeight = nextHeight + 1 err = blockExec.ValidateBlock(state, block) require.Error(t, err, "expected an error when state is ahead of block") assert.Contains(t, err.Error(), "lower than initial height") } func TestValidateBlockCommit(t *testing.T) { proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() //nolint:errcheck // ignore for tests state, stateDB, privVals := makeState(1, 1) stateStore := sm.NewStore(stateDB, sm.StoreOptions{ DiscardABCIResponses: false, }) mp := &mpmocks.Mempool{} mp.On("Lock").Return() mp.On("Unlock").Return() mp.On("FlushAppConn", mock.Anything).Return(nil) mp.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) blockStore := store.NewBlockStore(dbm.NewMemDB()) blockExec := sm.NewBlockExecutor( stateStore, log.TestingLogger(), proxyApp.Consensus(), mp, sm.EmptyEvidencePool{}, blockStore, ) lastCommit := &types.Commit{} var lastExtCommit *types.ExtendedCommit wrongSigsCommit := &types.Commit{Height: 1} badPrivVal := types.NewMockPV() for height := int64(1); height < validationTestsStopHeight; height++ { proposerAddr := state.Validators.GetProposer().Address if height > 1 { /* #2589: ensure state.LastValidators.VerifyCommit fails here */ // should be height-1 instead of height idx, _ := state.Validators.GetByAddress(proposerAddr) wrongHeightVote := types.MakeVoteNoError( t, privVals[proposerAddr.String()], chainID, idx, height, 0, 2, state.LastBlockID, time.Now(), ) wrongHeightCommit := &types.Commit{ Height: wrongHeightVote.Height, Round: wrongHeightVote.Round, BlockID: state.LastBlockID, Signatures: []types.CommitSig{wrongHeightVote.CommitSig()}, } block, err := makeBlock(state, height, wrongHeightCommit) require.NoError(t, err) err = blockExec.ValidateBlock(state, block) _, isErrInvalidCommitHeight := err.(types.ErrInvalidCommitHeight) require.True(t, isErrInvalidCommitHeight, "expected ErrInvalidCommitHeight at height %d but got: %v", height, err) /* #2589: test len(block.LastCommit.Signatures) == state.LastValidators.Size() */ _, err = makeBlock(state, height, wrongSigsCommit) require.Error(t, err) require.ErrorContains(t, err, "error making block") } /* A good block passes */ var err error var blockID types.BlockID state, blockID, lastExtCommit, err = makeAndCommitGoodBlock( state, height, lastCommit, proposerAddr, blockExec, privVals, nil, ) require.NoError(t, err, "height %d", height) lastCommit = lastExtCommit.ToCommit() /* wrongSigsCommit is fine except for the extra bad precommit */ idx, _ := state.Validators.GetByAddress(proposerAddr) goodVote := types.MakeVoteNoError( t, privVals[proposerAddr.String()], chainID, idx, height, 0, cmtproto.PrecommitType, blockID, time.Now(), ) bpvPubKey, err := badPrivVal.GetPubKey() require.NoError(t, err) badVote := &types.Vote{ ValidatorAddress: bpvPubKey.Address(), ValidatorIndex: 0, Height: height, Round: 0, Timestamp: cmttime.Now(), Type: cmtproto.PrecommitType, BlockID: blockID, } g := goodVote.ToProto() b := badVote.ToProto() err = badPrivVal.SignVote(chainID, g) require.NoError(t, err, "height %d", height) err = badPrivVal.SignVote(chainID, b) require.NoError(t, err, "height %d", height) goodVote.Signature, badVote.Signature = g.Signature, b.Signature wrongSigsCommit = &types.Commit{ Height: goodVote.Height, Round: goodVote.Round, BlockID: blockID, Signatures: []types.CommitSig{goodVote.CommitSig(), badVote.CommitSig()}, } } } func TestValidateBlockEvidence(t *testing.T) { proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() //nolint:errcheck // ignore for tests state, stateDB, privVals := makeState(4, 1) stateStore := sm.NewStore(stateDB, sm.StoreOptions{ DiscardABCIResponses: false, }) defaultEvidenceTime := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) evpool := &mocks.EvidencePool{} evpool.On("CheckEvidence", mock.AnythingOfType("types.EvidenceList")).Return(nil) evpool.On("Update", mock.AnythingOfType("state.State"), mock.AnythingOfType("types.EvidenceList")).Return() evpool.On("ABCIEvidence", mock.AnythingOfType("int64"), mock.AnythingOfType("[]types.Evidence")).Return( []abci.Misbehavior{}) mp := &mpmocks.Mempool{} mp.On("Lock").Return() mp.On("Unlock").Return() mp.On("FlushAppConn", mock.Anything).Return(nil) mp.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) state.ConsensusParams.Evidence.MaxBytes = 1000 blockStore := store.NewBlockStore(dbm.NewMemDB()) blockExec := sm.NewBlockExecutor( stateStore, log.TestingLogger(), proxyApp.Consensus(), mp, evpool, blockStore, ) lastCommit := &types.Commit{} var lastExtCommit *types.ExtendedCommit for height := int64(1); height < validationTestsStopHeight; height++ { proposerAddr := state.Validators.GetProposer().Address maxBytesEvidence := state.ConsensusParams.Evidence.MaxBytes if height > 1 { /* A block with too much evidence fails */ evidence := make([]types.Evidence, 0) var currentBytes int64 // more bytes than the maximum allowed for evidence for currentBytes <= maxBytesEvidence { newEv, err := types.NewMockDuplicateVoteEvidenceWithValidator(height, time.Now(), privVals[proposerAddr.String()], chainID) require.NoError(t, err) evidence = append(evidence, newEv) currentBytes += int64(len(newEv.Bytes())) } block, err := state.MakeBlock(height, test.MakeNTxs(height, 10), lastCommit, evidence, proposerAddr) require.NoError(t, err) err = blockExec.ValidateBlock(state, block) if assert.Error(t, err) { _, ok := err.(*types.ErrEvidenceOverflow) require.True(t, ok, "expected error to be of type ErrEvidenceOverflow at height %d but got %v", height, err) } } /* A good block with several pieces of good evidence passes */ evidence := make([]types.Evidence, 0) var currentBytes int64 // precisely the amount of allowed evidence for { newEv, err := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, privVals[proposerAddr.String()], chainID) require.NoError(t, err) currentBytes += int64(len(newEv.Bytes())) if currentBytes >= maxBytesEvidence { break } evidence = append(evidence, newEv) } var err error state, _, lastExtCommit, err = makeAndCommitGoodBlock( state, height, lastCommit, proposerAddr, blockExec, privVals, evidence, ) require.NoError(t, err, "height %d", height) lastCommit = lastExtCommit.ToCommit() } } func TestValidateBlockTime(t *testing.T) { proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() //nolint:errcheck // ignore for tests state, stateDB, privVals := makeState(3, 1) stateStore := sm.NewStore(stateDB, sm.StoreOptions{ DiscardABCIResponses: false, }) mp := &mpmocks.Mempool{} mp.On("Lock").Return() mp.On("Unlock").Return() mp.On("FlushAppConn", mock.Anything).Return(nil) mp.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) blockStore := store.NewBlockStore(dbm.NewMemDB()) blockExec := sm.NewBlockExecutor( stateStore, log.TestingLogger(), proxyApp.Consensus(), mp, sm.EmptyEvidencePool{}, blockStore, ) lastCommit := &types.Commit{} var lastExtCommit *types.ExtendedCommit // Build up state for test for height := int64(1); height < 3; height++ { var err error state, _, lastExtCommit, err = makeAndCommitGoodBlock( state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil) require.NoError(t, err, "height %d", height) lastCommit = lastExtCommit.ToCommit() } t.Run("block time before last block time", func(t *testing.T) { height := int64(3) block, err := makeBlock(state, height, lastCommit) require.NoError(t, err) // Set time to before last block time block.Time = block.Time.Add(-time.Millisecond * 10) err = blockExec.ValidateBlock(state, block) require.ErrorContains(t, err, "not greater than last block time") }) t.Run("block time after last block time, different than median time", func(t *testing.T) { height := int64(3) block, err := makeBlock(state, height, lastCommit) require.NoError(t, err) // Set time to after the median time block.Time = block.Time.Add(time.Second) err = blockExec.ValidateBlock(state, block) require.Error(t, err) require.Contains(t, err.Error(), "invalid block time") }) t.Run("block time after last block time, same as median time", func(t *testing.T) { height := int64(3) block, err := makeBlock(state, height, lastCommit) require.NoError(t, err) err = blockExec.ValidateBlock(state, block) require.NoError(t, err) }) } func TestValidateBlockInvalidCommit(t *testing.T) { proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() //nolint:errcheck // ignore for tests state, stateDB, privVals := makeState(3, 1) stateStore := sm.NewStore(stateDB, sm.StoreOptions{ DiscardABCIResponses: false, }) mp := &mpmocks.Mempool{} mp.On("Lock").Return() mp.On("Unlock").Return() mp.On("FlushAppConn", mock.Anything).Return(nil) mp.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) blockStore := store.NewBlockStore(dbm.NewMemDB()) blockExec := sm.NewBlockExecutor( stateStore, log.TestingLogger(), proxyApp.Consensus(), mp, sm.EmptyEvidencePool{}, blockStore, ) lastCommit := &types.Commit{} var lastExtCommit *types.ExtendedCommit // Build up state for test for height := int64(1); height < 3; height++ { var err error state, _, lastExtCommit, err = makeAndCommitGoodBlock( state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil) require.NoError(t, err, "height %d", height) lastCommit = lastExtCommit.ToCommit() } t.Run("commit with unknown validator flagged as commit", func(t *testing.T) { height := int64(3) // Create a commit where only unknown validators have BlockIDFlagCommit unknownVal := ed25519.GenPrivKey() now := time.Now() invalidCommit := &types.Commit{ Height: height - 1, Round: 0, BlockID: state.LastBlockID, Signatures: []types.CommitSig{ { BlockIDFlag: types.BlockIDFlagCommit, ValidatorAddress: unknownVal.PubKey().Address(), Timestamp: now, Signature: []byte("dummy"), }, }, } _, err := makeBlock(state, height, invalidCommit) require.Error(t, err) require.Contains(t, err.Error(), "commit validator not found in validator set") }) }