package inspect_test import ( "context" "fmt" "net" "os" "strings" "sync" "testing" "time" "github.com/fortytw2/leaktest" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" abcitypes "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/internal/inspect" "github.com/tendermint/tendermint/internal/state/indexer" indexermocks "github.com/tendermint/tendermint/internal/state/indexer/mocks" statemocks "github.com/tendermint/tendermint/internal/state/mocks" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/proto/tendermint/state" httpclient "github.com/tendermint/tendermint/rpc/client/http" "github.com/tendermint/tendermint/types" ) func TestInspectConstructor(t *testing.T) { cfg, err := config.ResetTestRoot("test") require.NoError(t, err) testLogger := log.TestingLogger() t.Cleanup(leaktest.Check(t)) defer func() { _ = os.RemoveAll(cfg.RootDir) }() t.Run("from config", func(t *testing.T) { logger := testLogger.With(t.Name()) d, err := inspect.NewFromConfig(logger, cfg) require.NoError(t, err) require.NotNil(t, d) }) } func TestInspectRun(t *testing.T) { cfg, err := config.ResetTestRoot("test") require.NoError(t, err) testLogger := log.TestingLogger() t.Cleanup(leaktest.Check(t)) defer func() { _ = os.RemoveAll(cfg.RootDir) }() t.Run("from config", func(t *testing.T) { logger := testLogger.With(t.Name()) d, err := inspect.NewFromConfig(logger, cfg) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) stoppedWG := &sync.WaitGroup{} stoppedWG.Add(1) go func() { require.NoError(t, d.Run(ctx)) stoppedWG.Done() }() cancel() stoppedWG.Wait() }) } func TestBlock(t *testing.T) { testHeight := int64(1) testBlock := new(types.Block) testBlock.Header.Height = testHeight testBlock.Header.LastCommitHash = []byte("test hash") stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Height").Return(testHeight) blockStoreMock.On("Base").Return(int64(0)) blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{}) blockStoreMock.On("LoadBlock", testHeight).Return(testBlock) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) resultBlock, err := cli.Block(ctx, &testHeight) require.NoError(t, err) require.Equal(t, testBlock.Height, resultBlock.Block.Height) require.Equal(t, testBlock.LastCommitHash, resultBlock.Block.LastCommitHash) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestTxSearch(t *testing.T) { testHash := []byte("test") testTx := []byte("tx") testQuery := fmt.Sprintf("tx.hash='%s'", string(testHash)) testTxResult := &abcitypes.TxResult{ Height: 1, Index: 100, Tx: testTx, } stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.KV) eventSinkMock.On("SearchTxEvents", mock.Anything, mock.MatchedBy(func(q *query.Query) bool { return testQuery == q.String() })). Return([]*abcitypes.TxResult{testTxResult}, nil) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) var page = 1 resultTxSearch, err := cli.TxSearch(ctx, testQuery, false, &page, &page, "") require.NoError(t, err) require.Len(t, resultTxSearch.Txs, 1) require.Equal(t, types.Tx(testTx), resultTxSearch.Txs[0].Tx) cancel() wg.Wait() eventSinkMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) blockStoreMock.AssertExpectations(t) } func TestTx(t *testing.T) { testHash := []byte("test") testTx := []byte("tx") stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.KV) eventSinkMock.On("GetTxByHash", testHash).Return(&abcitypes.TxResult{ Tx: testTx, }, nil) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) res, err := cli.Tx(ctx, testHash, false) require.NoError(t, err) require.Equal(t, types.Tx(testTx), res.Tx) cancel() wg.Wait() eventSinkMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) blockStoreMock.AssertExpectations(t) } func TestConsensusParams(t *testing.T) { testHeight := int64(1) testMaxGas := int64(55) stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Height").Return(testHeight) blockStoreMock.On("Base").Return(int64(0)) stateStoreMock.On("LoadConsensusParams", testHeight).Return(types.ConsensusParams{ Block: types.BlockParams{ MaxGas: testMaxGas, }, }, nil) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) params, err := cli.ConsensusParams(ctx, &testHeight) require.NoError(t, err) require.Equal(t, params.ConsensusParams.Block.MaxGas, testMaxGas) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestBlockResults(t *testing.T) { testHeight := int64(1) testGasUsed := int64(100) stateStoreMock := &statemocks.Store{} // tmstate "github.com/tendermint/tendermint/proto/tendermint/state" stateStoreMock.On("LoadABCIResponses", testHeight).Return(&state.ABCIResponses{ DeliverTxs: []*abcitypes.ResponseDeliverTx{ { GasUsed: testGasUsed, }, }, EndBlock: &abcitypes.ResponseEndBlock{}, BeginBlock: &abcitypes.ResponseBeginBlock{}, }, nil) blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Base").Return(int64(0)) blockStoreMock.On("Height").Return(testHeight) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) res, err := cli.BlockResults(ctx, &testHeight) require.NoError(t, err) require.Equal(t, res.TotalGasUsed, testGasUsed) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestCommit(t *testing.T) { testHeight := int64(1) testRound := int32(101) stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Base").Return(int64(0)) blockStoreMock.On("Height").Return(testHeight) blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{}, nil) blockStoreMock.On("LoadSeenCommit").Return(&types.Commit{ Height: testHeight, Round: testRound, }, nil) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) res, err := cli.Commit(ctx, &testHeight) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, res.SignedHeader.Commit.Round, testRound) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestBlockByHash(t *testing.T) { testHeight := int64(1) testHash := []byte("test hash") testBlock := new(types.Block) testBlock.Header.Height = testHeight testBlock.Header.LastCommitHash = testHash stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ BlockID: types.BlockID{ Hash: testHash, }, Header: types.Header{ Height: testHeight, }, }, nil) blockStoreMock.On("LoadBlockByHash", testHash).Return(testBlock, nil) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) res, err := cli.BlockByHash(ctx, testHash) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, []byte(res.BlockID.Hash), testHash) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestBlockchain(t *testing.T) { testHeight := int64(1) testBlock := new(types.Block) testBlockHash := []byte("test hash") testBlock.Header.Height = testHeight testBlock.Header.LastCommitHash = testBlockHash stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Height").Return(testHeight) blockStoreMock.On("Base").Return(int64(0)) blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ BlockID: types.BlockID{ Hash: testBlockHash, }, }) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) res, err := cli.BlockchainInfo(ctx, 0, 100) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, testBlockHash, []byte(res.BlockMetas[0].BlockID.Hash)) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestValidators(t *testing.T) { testHeight := int64(1) testVotingPower := int64(100) testValidators := types.ValidatorSet{ Validators: []*types.Validator{ { VotingPower: testVotingPower, }, }, } stateStoreMock := &statemocks.Store{} stateStoreMock.On("LoadValidators", testHeight).Return(&testValidators, nil) blockStoreMock := &statemocks.BlockStore{} blockStoreMock.On("Height").Return(testHeight) blockStoreMock.On("Base").Return(int64(0)) eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.EventSinkType("Mock")) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) testPage := 1 testPerPage := 100 res, err := cli.Validators(ctx, &testHeight, &testPage, &testPerPage) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, testVotingPower, res.Validators[0].VotingPower) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func TestBlockSearch(t *testing.T) { testHeight := int64(1) testBlockHash := []byte("test hash") testQuery := "block.height = 1" stateStoreMock := &statemocks.Store{} blockStoreMock := &statemocks.BlockStore{} eventSinkMock := &indexermocks.EventSink{} eventSinkMock.On("Stop").Return(nil) eventSinkMock.On("Type").Return(indexer.KV) blockStoreMock.On("LoadBlock", testHeight).Return(&types.Block{ Header: types.Header{ Height: testHeight, }, }, nil) blockStoreMock.On("LoadBlockMeta", testHeight).Return(&types.BlockMeta{ BlockID: types.BlockID{ Hash: testBlockHash, }, }) eventSinkMock.On("SearchBlockEvents", mock.Anything, mock.MatchedBy(func(q *query.Query) bool { return testQuery == q.String() })). Return([]int64{testHeight}, nil) rpcConfig := config.TestRPCConfig() l := log.TestingLogger() d := inspect.New(rpcConfig, blockStoreMock, stateStoreMock, []indexer.EventSink{eventSinkMock}, l) ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} wg.Add(1) startedWG := &sync.WaitGroup{} startedWG.Add(1) go func() { startedWG.Done() defer wg.Done() require.NoError(t, d.Run(ctx)) }() // FIXME: used to induce context switch. // Determine more deterministic method for prompting a context switch startedWG.Wait() requireConnect(t, rpcConfig.ListenAddress, 20) cli, err := httpclient.New(rpcConfig.ListenAddress) require.NoError(t, err) testPage := 1 testPerPage := 100 testOrderBy := "desc" res, err := cli.BlockSearch(ctx, testQuery, &testPage, &testPerPage, testOrderBy) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, testBlockHash, []byte(res.Blocks[0].BlockID.Hash)) cancel() wg.Wait() blockStoreMock.AssertExpectations(t) stateStoreMock.AssertExpectations(t) } func requireConnect(t testing.TB, addr string, retries int) { parts := strings.SplitN(addr, "://", 2) if len(parts) != 2 { t.Fatalf("malformed address to dial: %s", addr) } var err error for i := 0; i < retries; i++ { var conn net.Conn conn, err = net.Dial(parts[0], parts[1]) if err == nil { conn.Close() return } // FIXME attempt to yield and let the other goroutine continue execution. time.Sleep(time.Microsecond * 100) } t.Fatalf("unable to connect to server %s after %d tries: %s", addr, retries, err) }