Add gRPC server & reflection (#6463)
* Add gRPC proxy * Make GRPC disabled by default * WIP on integration tests * WIP on integration tests * Start setting up in process tests * Start setting up in process tests * Make it compile * Add start server to network util * Add Println * Use go routine * Fix scopelint * Move to proxy_test * Add response type cache * Remove proxy * Tweaks * Use channel to handle error * Use error chan * Update server/start.go Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com> * Use %w * Add sdk.Context * Add comments * Fix lint * Add header and tests * Address comments * Factorize some code * Fix lint * Add height and prove in req metadata * Add reflection test * Fix lint * Put grpc test in server/grpc * Update baseapp/grpcserver.go * Update baseapp/grpcserver.go * Remove proof header Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com> Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com> Co-authored-by: SaReN <sahithnarahari@gmail.com>
This commit is contained in:
co-authored by
Federico Kunze
Amaury Martiny
Alexander Bezobchuk
SaReN
parent
20488b4f8e
commit
e9534b0935
@@ -75,6 +75,15 @@ type APIConfig struct {
|
||||
// Ref: https://github.com/cosmos/cosmos-sdk/issues/6420
|
||||
}
|
||||
|
||||
// GRPCConfig defines configuration for the gRPC server.
|
||||
type GRPCConfig struct {
|
||||
// Enable defines if the gRPC server should be enabled.
|
||||
Enable bool `mapstructure:"enable"`
|
||||
|
||||
// Address defines the API server to listen on
|
||||
Address string `mapstructure:"address"`
|
||||
}
|
||||
|
||||
// Config defines the server's top level configuration
|
||||
type Config struct {
|
||||
BaseConfig `mapstructure:",squash"`
|
||||
@@ -82,6 +91,7 @@ type Config struct {
|
||||
// Telemetry defines the application telemetry configuration
|
||||
Telemetry telemetry.Config `mapstructure:"telemetry"`
|
||||
API APIConfig `mapstructure:"api"`
|
||||
GRPC GRPCConfig `mapstructure:"grpc"`
|
||||
}
|
||||
|
||||
// SetMinGasPrices sets the validator's minimum gas prices.
|
||||
@@ -134,6 +144,10 @@ func DefaultConfig() *Config {
|
||||
RPCReadTimeout: 10,
|
||||
RPCMaxBodyBytes: 1000000,
|
||||
},
|
||||
GRPC: GRPCConfig{
|
||||
Enable: false,
|
||||
Address: "0.0.0.0:9090",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,5 +191,9 @@ func GetConfig(v *viper.Viper) Config {
|
||||
RPCMaxBodyBytes: v.GetUint("api.rpc-max-body-bytes"),
|
||||
EnableUnsafeCORS: v.GetBool("api.enabled-unsafe-cors"),
|
||||
},
|
||||
GRPC: GRPCConfig{
|
||||
Enable: v.GetBool("grpc.enable"),
|
||||
Address: v.GetString("grpc.address"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/api"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// AppOptions defines an interface that is passed into an application
|
||||
// constructor, typically used to set BaseApp options that are either supplied
|
||||
// via config file or through CLI arguments/flags. The underlying implementation
|
||||
// is defined by the server package and is typically implemented via a Viper
|
||||
// literal defined on the server Context. Note, casting Get calls may not yield
|
||||
// the expected types and could result in type assertion errors. It is recommend
|
||||
// to either use the cast package or perform manual conversion for safety.
|
||||
AppOptions interface {
|
||||
Get(string) interface{}
|
||||
}
|
||||
|
||||
// Application defines an application interface that wraps abci.Application.
|
||||
// The interface defines the necessary contracts to be implemented in order
|
||||
// to fully bootstrap and start an application.
|
||||
Application interface {
|
||||
abci.Application
|
||||
|
||||
RegisterAPIRoutes(*api.Server)
|
||||
}
|
||||
|
||||
// AppCreator is a function that allows us to lazily initialize an
|
||||
// application using various configurations.
|
||||
AppCreator func(log.Logger, dbm.DB, io.Writer, AppOptions) Application
|
||||
|
||||
// AppExporter is a function that dumps all app state to
|
||||
// JSON-serializable structure and returns the current validator set.
|
||||
AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error)
|
||||
)
|
||||
|
||||
func openDB(rootDir string) (dbm.DB, error) {
|
||||
dataDir := filepath.Join(rootDir, "data")
|
||||
db, err := sdk.NewLevelDB("application", dataDir)
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
@@ -23,7 +24,7 @@ const (
|
||||
)
|
||||
|
||||
// ExportCmd dumps app state to JSON.
|
||||
func ExportCmd(appExporter AppExporter, defaultNodeHome string) *cobra.Command {
|
||||
func ExportCmd(appExporter types.AppExporter, defaultNodeHome string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export state to JSON",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// GRPCBlockHeightHeader is the gRPC header for block height.
|
||||
GRPCBlockHeightHeader = "x-cosmos-block-height"
|
||||
)
|
||||
|
||||
// StartGRPCServer starts a gRPC server on the given address.
|
||||
func StartGRPCServer(app types.Application, address string) (*grpc.Server, error) {
|
||||
grpcSrv := grpc.NewServer()
|
||||
app.RegisterGRPCServer(grpcSrv)
|
||||
|
||||
// Reflection allows external clients to see what services and methods
|
||||
// the gRPC server exposes.
|
||||
reflection.Register(grpcSrv)
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
err = grpcSrv.Serve(listener)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("failed to serve: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return nil, err
|
||||
case <-time.After(5 * time.Second): // assume server started successfully
|
||||
return grpcSrv, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
servergrpc "github.com/cosmos/cosmos-sdk/server/grpc"
|
||||
"github.com/cosmos/cosmos-sdk/testutil/network"
|
||||
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
network *network.Network
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) SetupSuite() {
|
||||
s.T().Log("setting up integration test suite")
|
||||
|
||||
s.network = network.New(s.T(), network.DefaultConfig())
|
||||
s.Require().NotNil(s.network)
|
||||
|
||||
_, err := s.network.WaitForHeight(2)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TearDownSuite() {
|
||||
s.T().Log("tearing down integration test suite")
|
||||
s.network.Cleanup()
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestGRPC() {
|
||||
val0 := s.network.Validators[0]
|
||||
conn, err := grpc.Dial(
|
||||
val0.AppConfig.GRPC.Address,
|
||||
grpc.WithInsecure(), // Or else we get "no transport security set"
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// gRPC query to test service should work
|
||||
testClient := testdata.NewTestServiceClient(conn)
|
||||
testRes, err := testClient.Echo(context.Background(), &testdata.EchoRequest{Message: "hello"})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("hello", testRes.Message)
|
||||
|
||||
// gRPC query to bank service should work
|
||||
denom := fmt.Sprintf("%stoken", val0.Moniker)
|
||||
bankClient := banktypes.NewQueryClient(conn)
|
||||
var header metadata.MD
|
||||
bankRes, err := bankClient.Balance(
|
||||
context.Background(),
|
||||
&banktypes.QueryBalanceRequest{Address: val0.Address, Denom: denom},
|
||||
grpc.Header(&header), // Also fetch grpc header
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(
|
||||
sdk.NewCoin(denom, s.network.Config.AccountTokens),
|
||||
*bankRes.GetBalance(),
|
||||
)
|
||||
blockHeight := header.Get(servergrpc.GRPCBlockHeightHeader)
|
||||
s.Require().Equal([]string{"2"}, blockHeight)
|
||||
|
||||
// Request metadata should work
|
||||
bankRes, err = bankClient.Balance(
|
||||
metadata.AppendToOutgoingContext(context.Background(), servergrpc.GRPCBlockHeightHeader, "1"), // Add metadata to request
|
||||
&banktypes.QueryBalanceRequest{Address: val0.Address, Denom: denom},
|
||||
grpc.Header(&header),
|
||||
)
|
||||
blockHeight = header.Get(servergrpc.GRPCBlockHeightHeader)
|
||||
s.Require().Equal([]string{"1"}, blockHeight)
|
||||
|
||||
// Test server reflection
|
||||
reflectClient := rpb.NewServerReflectionClient(conn)
|
||||
stream, err := reflectClient.ServerReflectionInfo(context.Background(), grpc.WaitForReady(true))
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(stream.Send(&rpb.ServerReflectionRequest{
|
||||
MessageRequest: &rpb.ServerReflectionRequest_ListServices{},
|
||||
}))
|
||||
res, err := stream.Recv()
|
||||
s.Require().NoError(err)
|
||||
services := res.GetListServicesResponse().Service
|
||||
servicesMap := make(map[string]bool)
|
||||
for _, s := range services {
|
||||
servicesMap[s.Name] = true
|
||||
}
|
||||
// Make sure the following services are present
|
||||
s.Require().True(servicesMap["cosmos.bank.Query"])
|
||||
}
|
||||
|
||||
func TestIntegrationTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
+7
-6
@@ -6,22 +6,23 @@ import (
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
"github.com/cosmos/cosmos-sdk/store/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
)
|
||||
|
||||
// GetPruningOptionsFromFlags parses command flags and returns the correct
|
||||
// PruningOptions. If a pruning strategy is provided, that will be parsed and
|
||||
// returned, otherwise, it is assumed custom pruning options are provided.
|
||||
func GetPruningOptionsFromFlags(appOpts AppOptions) (types.PruningOptions, error) {
|
||||
func GetPruningOptionsFromFlags(appOpts types.AppOptions) (storetypes.PruningOptions, error) {
|
||||
strategy := strings.ToLower(cast.ToString(appOpts.Get(FlagPruning)))
|
||||
|
||||
switch strategy {
|
||||
case types.PruningOptionDefault, types.PruningOptionNothing, types.PruningOptionEverything:
|
||||
return types.NewPruningOptionsFromString(strategy), nil
|
||||
case storetypes.PruningOptionDefault, storetypes.PruningOptionNothing, storetypes.PruningOptionEverything:
|
||||
return storetypes.NewPruningOptionsFromString(strategy), nil
|
||||
|
||||
case types.PruningOptionCustom:
|
||||
opts := types.NewPruningOptions(
|
||||
case storetypes.PruningOptionCustom:
|
||||
opts := storetypes.NewPruningOptions(
|
||||
cast.ToUint64(appOpts.Get(FlagPruningKeepRecent)),
|
||||
cast.ToUint64(appOpts.Get(FlagPruningKeepEvery)),
|
||||
cast.ToUint64(appOpts.Get(FlagPruningInterval)),
|
||||
|
||||
+18
-3
@@ -17,12 +17,15 @@ import (
|
||||
pvm "github.com/tendermint/tendermint/privval"
|
||||
"github.com/tendermint/tendermint/proxy"
|
||||
"github.com/tendermint/tendermint/rpc/client/local"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/server/api"
|
||||
"github.com/cosmos/cosmos-sdk/server/config"
|
||||
servergrpc "github.com/cosmos/cosmos-sdk/server/grpc"
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
)
|
||||
|
||||
@@ -49,7 +52,7 @@ const (
|
||||
|
||||
// StartCmd runs the service passed in, either stand-alone or in-process with
|
||||
// Tendermint.
|
||||
func StartCmd(appCreator AppCreator, defaultNodeHome string) *cobra.Command {
|
||||
func StartCmd(appCreator types.AppCreator, defaultNodeHome string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Run the full node",
|
||||
@@ -125,7 +128,7 @@ which accepts a path for the resulting pprof file.
|
||||
return cmd
|
||||
}
|
||||
|
||||
func startStandAlone(ctx *Context, appCreator AppCreator) error {
|
||||
func startStandAlone(ctx *Context, appCreator types.AppCreator) error {
|
||||
addr := ctx.Viper.GetString(flagAddress)
|
||||
transport := ctx.Viper.GetString(flagTransport)
|
||||
home := ctx.Viper.GetString(flags.FlagHome)
|
||||
@@ -165,7 +168,7 @@ func startStandAlone(ctx *Context, appCreator AppCreator) error {
|
||||
select {}
|
||||
}
|
||||
|
||||
func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator) error {
|
||||
func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator types.AppCreator) error {
|
||||
cfg := ctx.Config
|
||||
home := cfg.RootDir
|
||||
|
||||
@@ -239,6 +242,14 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator
|
||||
}
|
||||
}
|
||||
|
||||
var grpcSrv *grpc.Server
|
||||
if config.GRPC.Enable {
|
||||
grpcSrv, err = servergrpc.StartGRPCServer(app, config.GRPC.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var cpuProfileCleanup func()
|
||||
|
||||
if cpuProfile := ctx.Viper.GetString(flagCPUProfile); cpuProfile != "" {
|
||||
@@ -272,6 +283,10 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator
|
||||
_ = apiSrv.Close()
|
||||
}
|
||||
|
||||
if grpcSrv != nil {
|
||||
grpcSrv.Stop()
|
||||
}
|
||||
|
||||
ctx.Logger.Info("exiting...")
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/gogo/protobuf/grpc"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/api"
|
||||
)
|
||||
|
||||
type (
|
||||
// AppOptions defines an interface that is passed into an application
|
||||
// constructor, typically used to set BaseApp options that are either supplied
|
||||
// via config file or through CLI arguments/flags. The underlying implementation
|
||||
// is defined by the server package and is typically implemented via a Viper
|
||||
// literal defined on the server Context. Note, casting Get calls may not yield
|
||||
// the expected types and could result in type assertion errors. It is recommend
|
||||
// to either use the cast package or perform manual conversion for safety.
|
||||
AppOptions interface {
|
||||
Get(string) interface{}
|
||||
}
|
||||
|
||||
// Application defines an application interface that wraps abci.Application.
|
||||
// The interface defines the necessary contracts to be implemented in order
|
||||
// to fully bootstrap and start an application.
|
||||
Application interface {
|
||||
abci.Application
|
||||
|
||||
RegisterAPIRoutes(*api.Server)
|
||||
|
||||
// RegisterGRPCServer registers gRPC services directly with the gRPC
|
||||
// server.
|
||||
RegisterGRPCServer(grpc.Server)
|
||||
}
|
||||
|
||||
// AppCreator is a function that allows us to lazily initialize an
|
||||
// application using various configurations.
|
||||
AppCreator func(log.Logger, dbm.DB, io.Writer, AppOptions) Application
|
||||
|
||||
// AppExporter is a function that dumps all app state to
|
||||
// JSON-serializable structure and returns the current validator set.
|
||||
AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error)
|
||||
)
|
||||
+2
-1
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/server/config"
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
)
|
||||
@@ -167,7 +168,7 @@ func interceptConfigs(ctx *Context, rootViper *viper.Viper) (*tmcfg.Config, erro
|
||||
}
|
||||
|
||||
// add server commands
|
||||
func AddCommands(rootCmd *cobra.Command, appCreator AppCreator, appExport AppExporter) {
|
||||
func AddCommands(rootCmd *cobra.Command, appCreator types.AppCreator, appExport types.AppExporter) {
|
||||
tendermintCmd := &cobra.Command{
|
||||
Use: "tendermint",
|
||||
Short: "Tendermint subcommands",
|
||||
|
||||
Reference in New Issue
Block a user