package v0 import ( "fmt" "os" "sort" "testing" "time" "github.com/stretchr/testify/assert" 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" "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 } type BlockchainReactorPair struct { reactor *BlockchainReactor app proxy.AppConns } func newBlockchainReactor( logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockchainReactorPair { 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, 0, types.BlockID{}, nil) if blockHeight > 1 { lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) lastBlock := blockStore.LoadBlock(blockHeight - 1) vote, err := types.MakeVote( lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVals[0], lastBlock.Header.ChainID, time.Now(), ) if err != nil { panic(err) } 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 BlockchainReactorPair{bcReactor, proxyApp} } func TestNoBlockResponse(t *testing.T) { config = cfg.ResetTestRoot("blockchain_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) maxBlockHeight := int64(65) reactorPairs := make([]BlockchainReactorPair, 2) reactorPairs[0] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) reactorPairs[1] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) return s }, p2p.Connect2Switches) defer func() { for _, r := range reactorPairs { r.reactor.Stop() r.app.Stop() } }() tests := []struct { height int64 existent bool }{ {maxBlockHeight + 2, false}, {10, true}, {1, true}, {100, false}, } for { if reactorPairs[1].reactor.pool.IsCaughtUp() { break } time.Sleep(10 * time.Millisecond) } assert.Equal(t, maxBlockHeight, reactorPairs[0].reactor.store.Height()) for _, tt := range tests { block := reactorPairs[1].reactor.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 TestBadBlockStopsPeer(t *testing.T) { config = cfg.ResetTestRoot("blockchain_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) maxBlockHeight := int64(148) otherChain := newBlockchainReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) defer func() { otherChain.reactor.Stop() otherChain.app.Stop() }() reactorPairs := make([]BlockchainReactorPair, 4) reactorPairs[0] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) reactorPairs[1] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) reactorPairs[2] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) reactorPairs[3] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) switches := p2p.MakeConnectedSwitches(config.P2P, 4, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) return s }, p2p.Connect2Switches) defer func() { for _, r := range reactorPairs { r.reactor.Stop() r.app.Stop() } }() for { time.Sleep(1 * time.Second) caughtUp := true for _, r := range reactorPairs { if !r.reactor.pool.IsCaughtUp() { caughtUp = false } } if caughtUp { break } } //at this time, reactors[0-3] is the newest assert.Equal(t, 3, reactorPairs[1].reactor.Switch.Peers().Size()) // Mark reactorPairs[3] as an invalid peer. Fiddling with .store without a mutex is a data // race, but can't be easily avoided. reactorPairs[3].reactor.store = otherChain.reactor.store lastReactorPair := newBlockchainReactor(log.TestingLogger(), 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].reactor) return s }, p2p.Connect2Switches)...) for i := 0; i < len(reactorPairs)-1; i++ { p2p.Connect2Switches(switches, i, len(reactorPairs)-1) } for { if lastReactorPair.reactor.pool.IsCaughtUp() || lastReactorPair.reactor.Switch.Peers().Size() == 0 { break } time.Sleep(1 * time.Second) } assert.True(t, lastReactorPair.reactor.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 } var _ abci.Application = (*testApp)(nil) func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { return abci.ResponseInfo{} } func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { return abci.ResponseBeginBlock{} } func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { return abci.ResponseEndBlock{} } func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Events: []abci.Event{}} } func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } func (app *testApp) Commit() abci.ResponseCommit { return abci.ResponseCommit{} } func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { return }