package v1 import ( "fmt" "os" "sort" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mempool/mock" "github.com/tendermint/tendermint/p2p" tmproto "github.com/tendermint/tendermint/proto/types" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) var config *cfg.Config func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { validators := make([]types.GenesisValidator, numValidators) privValidators := make([]types.PrivValidator, numValidators) for i := 0; i < numValidators; i++ { val, privVal := types.RandValidator(randPower, minPower) validators[i] = types.GenesisValidator{ PubKey: val.PubKey, Power: val.VotingPower, } privValidators[i] = privVal } sort.Sort(types.PrivValidatorsByAddress(privValidators)) return &types.GenesisDoc{ GenesisTime: tmtime.Now(), ChainID: config.ChainID(), Validators: validators, }, privValidators } func makeVote( t *testing.T, header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { pubKey, err := privVal.GetPubKey() require.NoError(t, err) valIdx, _ := valset.GetByAddress(pubKey.Address()) vote := &types.Vote{ ValidatorAddress: pubKey.Address(), ValidatorIndex: valIdx, Height: header.Height, Round: 1, Timestamp: tmtime.Now(), Type: tmproto.PrecommitType, BlockID: blockID, } _ = privVal.SignVote(header.ChainID, vote) return vote } type BlockchainReactorPair struct { bcR *BlockchainReactor conR *consensusReactorTest } func newBlockchainReactor( t *testing.T, logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) *BlockchainReactor { if len(privVals) != 1 { panic("only support one validator") } app := &testApp{} cc := proxy.NewLocalClientCreator(app) proxyApp := proxy.NewAppConns(cc) err := proxyApp.Start() if err != nil { panic(fmt.Errorf("error start app: %w", err)) } blockDB := dbm.NewMemDB() stateDB := dbm.NewMemDB() blockStore := store.NewBlockStore(blockDB) state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { panic(fmt.Errorf("error constructing state from genesis file: %w", err)) } // Make the BlockchainReactor itself. // NOTE we have to create and commit the blocks first because // pool.height is determined from the store. fastSync := true db := dbm.NewMemDB() blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) sm.SaveState(db, state) // let's add some blocks in for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { lastCommit := types.NewCommit(blockHeight-1, 1, types.BlockID{}, nil) if blockHeight > 1 { lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) lastBlock := blockStore.LoadBlock(blockHeight - 1) vote := makeVote(t, &lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]) lastCommit = types.NewCommit(vote.Height, vote.Round, lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()}) } thisBlock := makeBlock(blockHeight, state, lastCommit) thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} state, _, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { panic(fmt.Errorf("error apply block: %w", err)) } blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) bcReactor.SetLogger(logger.With("module", "blockchain")) return bcReactor } func newBlockchainReactorPair( t *testing.T, logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockchainReactorPair { consensusReactor := &consensusReactorTest{} consensusReactor.BaseReactor = *p2p.NewBaseReactor("Consensus reactor", consensusReactor) return BlockchainReactorPair{ newBlockchainReactor(t, logger, genDoc, privVals, maxBlockHeight), consensusReactor} } type consensusReactorTest struct { p2p.BaseReactor // BaseService + p2p.Switch switchedToConsensus bool mtx sync.Mutex } func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced bool) { conR.mtx.Lock() defer conR.mtx.Unlock() conR.switchedToConsensus = true } func TestFastSyncNoBlockResponse(t *testing.T) { config = cfg.ResetTestRoot("blockchain_new_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) maxBlockHeight := int64(65) reactorPairs := make([]BlockchainReactorPair, 2) logger := log.TestingLogger() reactorPairs[0] = newBlockchainReactorPair(t, logger, genDoc, privVals, maxBlockHeight) reactorPairs[1] = newBlockchainReactorPair(t, logger, genDoc, privVals, 0) p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) s.AddReactor("CONSENSUS", reactorPairs[i].conR) moduleName := fmt.Sprintf("blockchain-%v", i) reactorPairs[i].bcR.SetLogger(logger.With("module", moduleName)) return s }, p2p.Connect2Switches) defer func() { for _, r := range reactorPairs { _ = r.bcR.Stop() _ = r.conR.Stop() } }() tests := []struct { height int64 existent bool }{ {maxBlockHeight + 2, false}, {10, true}, {1, true}, {maxBlockHeight + 100, false}, } for { time.Sleep(10 * time.Millisecond) reactorPairs[1].conR.mtx.Lock() if reactorPairs[1].conR.switchedToConsensus { reactorPairs[1].conR.mtx.Unlock() break } reactorPairs[1].conR.mtx.Unlock() } assert.Equal(t, maxBlockHeight, reactorPairs[0].bcR.store.Height()) for _, tt := range tests { block := reactorPairs[1].bcR.store.LoadBlock(tt.height) if tt.existent { assert.True(t, block != nil) } else { assert.True(t, block == nil) } } } // NOTE: This is too hard to test without // an easy way to add test peer to switch // or without significant refactoring of the module. // Alternatively we could actually dial a TCP conn but // that seems extreme. func TestFastSyncBadBlockStopsPeer(t *testing.T) { numNodes := 4 maxBlockHeight := int64(148) config = cfg.ResetTestRoot("blockchain_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) otherChain := newBlockchainReactorPair(t, log.TestingLogger(), genDoc, privVals, maxBlockHeight) defer func() { _ = otherChain.bcR.Stop() _ = otherChain.conR.Stop() }() reactorPairs := make([]BlockchainReactorPair, numNodes) logger := make([]log.Logger, numNodes) for i := 0; i < numNodes; i++ { logger[i] = log.TestingLogger() height := int64(0) if i == 0 { height = maxBlockHeight } reactorPairs[i] = newBlockchainReactorPair(t, logger[i], genDoc, privVals, height) } switches := p2p.MakeConnectedSwitches(config.P2P, numNodes, func(i int, s *p2p.Switch) *p2p.Switch { reactorPairs[i].conR.mtx.Lock() s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) s.AddReactor("CONSENSUS", reactorPairs[i].conR) moduleName := fmt.Sprintf("blockchain-%v", i) reactorPairs[i].bcR.SetLogger(logger[i].With("module", moduleName)) reactorPairs[i].conR.mtx.Unlock() return s }, p2p.Connect2Switches) defer func() { for _, r := range reactorPairs { _ = r.bcR.Stop() _ = r.conR.Stop() } }() outerFor: for { time.Sleep(10 * time.Millisecond) for i := 0; i < numNodes; i++ { reactorPairs[i].conR.mtx.Lock() if !reactorPairs[i].conR.switchedToConsensus { reactorPairs[i].conR.mtx.Unlock() continue outerFor } reactorPairs[i].conR.mtx.Unlock() } break } //at this time, reactors[0-3] is the newest assert.Equal(t, numNodes-1, reactorPairs[1].bcR.Switch.Peers().Size()) //mark last reactorPair as an invalid peer reactorPairs[numNodes-1].bcR.store = otherChain.bcR.store lastLogger := log.TestingLogger() lastReactorPair := newBlockchainReactorPair(t, lastLogger, genDoc, privVals, 0) reactorPairs = append(reactorPairs, lastReactorPair) switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[len(reactorPairs)-1].bcR) s.AddReactor("CONSENSUS", reactorPairs[len(reactorPairs)-1].conR) moduleName := fmt.Sprintf("blockchain-%v", len(reactorPairs)-1) reactorPairs[len(reactorPairs)-1].bcR.SetLogger(lastLogger.With("module", moduleName)) return s }, p2p.Connect2Switches)...) for i := 0; i < len(reactorPairs)-1; i++ { p2p.Connect2Switches(switches, i, len(reactorPairs)-1) } for { time.Sleep(1 * time.Second) lastReactorPair.conR.mtx.Lock() if lastReactorPair.conR.switchedToConsensus { lastReactorPair.conR.mtx.Unlock() break } lastReactorPair.conR.mtx.Unlock() if lastReactorPair.bcR.Switch.Peers().Size() == 0 { break } } assert.True(t, lastReactorPair.bcR.Switch.Peers().Size() < len(reactorPairs)-1) } func TestBcBlockRequestMessageValidateBasic(t *testing.T) { testCases := []struct { testName string requestHeight int64 expectErr bool }{ {"Valid Request Message", 0, false}, {"Valid Request Message", 1, false}, {"Invalid Request Message", -1, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { request := bcBlockRequestMessage{Height: tc.requestHeight} assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { testCases := []struct { testName string nonResponseHeight int64 expectErr bool }{ {"Valid Non-Response Message", 0, false}, {"Valid Non-Response Message", 1, false}, {"Invalid Non-Response Message", -1, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } func TestBcStatusRequestMessageValidateBasic(t *testing.T) { testCases := []struct { testName string requestHeight int64 expectErr bool }{ {"Valid Request Message", 0, false}, {"Valid Request Message", 1, false}, {"Invalid Request Message", -1, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { request := bcStatusRequestMessage{Height: tc.requestHeight} assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } func TestBcStatusResponseMessageValidateBasic(t *testing.T) { testCases := []struct { testName string responseHeight int64 expectErr bool }{ {"Valid Response Message", 0, false}, {"Valid Response Message", 1, false}, {"Invalid Response Message", -1, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { response := bcStatusResponseMessage{Height: tc.responseHeight} assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } //---------------------------------------------- // utility funcs func makeTxs(height int64) (txs []types.Tx) { for i := 0; i < 10; i++ { txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) } return txs } func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) return block } type testApp struct { abci.BaseApplication }