From 9cc1a5afe495a1a1ce95d30cccbb9dd48bf96923 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Wed, 21 Feb 2024 14:19:56 +0100 Subject: [PATCH] Implement grpc query --- .../keeper/query_plugin_integration_test.go | 53 +++++++++++++++ x/wasm/keeper/query_plugins.go | 65 +++++++++++++++++-- x/wasm/keeper/query_plugins_test.go | 2 +- 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/x/wasm/keeper/query_plugin_integration_test.go b/x/wasm/keeper/query_plugin_integration_test.go index 4ff5954c82..f9d126e20e 100644 --- a/x/wasm/keeper/query_plugin_integration_test.go +++ b/x/wasm/keeper/query_plugin_integration_test.go @@ -3,6 +3,7 @@ package keeper import ( "bytes" "encoding/json" + "errors" "fmt" "strings" "testing" @@ -116,6 +117,58 @@ func TestReflectStargateQuery(t *testing.T) { assert.Equal(t, simpleBalance.Amount[0].Denom, expectedBalance[0].Denom) } +func TestReflectGrpcQuery(t *testing.T) { + queryPlugins := (*reflectPlugins()).Merge(&QueryPlugins{ + Grpc: func(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (proto.Message, error) { + if request.Path == "cosmos.bank.v1beta1.Query/AllBalances" { + return &banktypes.QueryAllBalancesResponse{ + Balances: sdk.NewCoins(), + }, nil + } + return nil, errors.New("unsupported request") + }, + }) + cdc := MakeEncodingConfig(t).Codec + ctx, keepers := CreateTestInput(t, false, ReflectCapabilities, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(&queryPlugins)) + keeper := keepers.WasmKeeper + + creator := RandomAccountAddress(t) + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", sdk.NewCoins()) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // now grpc query for the bank balance + cosmosBankQuery := banktypes.NewQueryAllBalancesRequest(creator, nil, false) + cosmosBankQueryBz, err := proto.Marshal(cosmosBankQuery) + require.NoError(t, err) + reflectQuery := wasmvmtypes.QueryRequest{ + Grpc: &wasmvmtypes.GrpcQuery{ + Path: "cosmos.bank.v1beta1.Query/AllBalances", + Data: cosmosBankQueryBz, + }, + } + reflectQueryBz, err := json.Marshal(testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{Request: &reflectQuery}, + }) + require.NoError(t, err) + // query the reflect contract + reflectRespBz, err := keeper.QuerySmart(ctx, contractAddr, reflectQueryBz) + require.NoError(t, err) + var reflectResp testdata.ChainResponse + mustUnmarshal(t, reflectRespBz, &reflectResp) + // now unmarshal the protobuf response + var grpcBalance banktypes.QueryAllBalancesResponse + err = proto.Unmarshal(reflectResp.Data, &grpcBalance) + require.NoError(t, err) +} + func TestReflectTotalSupplyQuery(t *testing.T) { cdc := MakeEncodingConfig(t).Codec ctx, keepers := CreateTestInput(t, false, ReflectCapabilities, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) diff --git a/x/wasm/keeper/query_plugins.go b/x/wasm/keeper/query_plugins.go index 488e4d3adb..ee62da2f51 100644 --- a/x/wasm/keeper/query_plugins.go +++ b/x/wasm/keeper/query_plugins.go @@ -90,6 +90,7 @@ type QueryPlugins struct { IBC func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) Staking func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) Stargate func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) + Grpc func(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (proto.Message, error) Wasm func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) Distribution func(ctx sdk.Context, request *wasmvmtypes.DistributionQuery) ([]byte, error) } @@ -113,12 +114,15 @@ func DefaultQueryPlugins( channelKeeper types.ChannelKeeper, wasm wasmQueryKeeper, ) QueryPlugins { + // By default, we reject all stargate and gRPC queries. + // The chain needs to provide a querier plugin that only allows deterministic queries. return QueryPlugins{ Bank: BankQuerier(bank), Custom: NoCustomQuerier, IBC: IBCQuerier(wasm, channelKeeper), Staking: StakingQuerier(staking, distKeeper), Stargate: RejectStargateQuerier(), + Grpc: RejectGrpcQuerier, Wasm: WasmQuerier(wasm), Distribution: DistributionQuerier(distKeeper), } @@ -144,6 +148,9 @@ func (e QueryPlugins) Merge(o *QueryPlugins) QueryPlugins { if o.Stargate != nil { e.Stargate = o.Stargate } + if o.Grpc != nil { + e.Grpc = o.Grpc + } if o.Wasm != nil { e.Wasm = o.Wasm } @@ -167,6 +174,14 @@ func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, req wa return e.Staking(ctx, req.Staking) case req.Stargate != nil: return e.Stargate(ctx, req.Stargate) + case req.Grpc != nil: + resp, err := e.Grpc(ctx, req.Grpc) + if err != nil { + return nil, err + } + // Marshalling the response here instead of inside the query + // plugin makes sure that the response is always protobuf encoded. + return proto.Marshal(resp) case req.Wasm != nil: return e.Wasm(ctx, req.Wasm) case req.Distribution != nil: @@ -317,6 +332,48 @@ func IBCQuerier(wasm contractMetaDataSource, channelKeeper types.ChannelKeeper) } } +func RejectGrpcQuerier(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (proto.Message, error) { + return nil, wasmvmtypes.UnsupportedRequest{Kind: "gRPC queries are disabled"} +} + +// AcceptListGrpcQuerier supports a preconfigured set of gRPC queries only. +// All arguments must be non nil. +// +// Warning: Chains need to test and maintain their accept list carefully. +// There were critical consensus breaking issues in the past with non-deterministic behavior in the SDK. +// +// These queries can be set via WithQueryPlugins option in the wasm keeper constructor: +// WithQueryPlugins(&QueryPlugins{Grpc: AcceptListGrpcQuerier(acceptList, queryRouter, codec)}) +func AcceptListGrpcQuerier(acceptList AcceptedQueries, queryRouter GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (proto.Message, error) { + return func(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (proto.Message, error) { + protoResponse, accepted := acceptList[request.Path] + if !accepted { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)} + } + + handler := queryRouter.Route(request.Path) + if handler == nil { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)} + } + + res, err := handler(ctx, &abci.RequestQuery{ + Data: request.Data, + Path: request.Path, + }) + if err != nil { + return nil, err + } + + // decode the query response into the expected protobuf message + err = codec.Unmarshal(res.Value, protoResponse) + if err != nil { + return nil, err + } + + return protoResponse, nil + } +} + // RejectStargateQuerier rejects all stargate queries func RejectStargateQuerier() func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { @@ -324,10 +381,10 @@ func RejectStargateQuerier() func(ctx sdk.Context, request *wasmvmtypes.Stargate } } -// AcceptedStargateQueries define accepted Stargate queries as a map with path as key and response type as value. +// AcceptedQueries define accepted Stargate or gRPC queries as a map with path as key and response type as value. // For example: // acceptList["/cosmos.auth.v1beta1.Query/Account"]= &authtypes.QueryAccountResponse{} -type AcceptedStargateQueries map[string]proto.Message +type AcceptedQueries map[string]proto.Message // AcceptListStargateQuerier supports a preconfigured set of stargate queries only. // All arguments must be non nil. @@ -335,9 +392,9 @@ type AcceptedStargateQueries map[string]proto.Message // Warning: Chains need to test and maintain their accept list carefully. // There were critical consensus breaking issues in the past with non-deterministic behavior in the SDK. // -// This queries can be set via WithQueryPlugins option in the wasm keeper constructor: +// These queries can be set via WithQueryPlugins option in the wasm keeper constructor: // WithQueryPlugins(&QueryPlugins{Stargate: AcceptListStargateQuerier(acceptList, queryRouter, codec)}) -func AcceptListStargateQuerier(acceptList AcceptedStargateQueries, queryRouter GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { +func AcceptListStargateQuerier(acceptList AcceptedQueries, queryRouter GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { protoResponse, accepted := acceptList[request.Path] if !accepted { diff --git a/x/wasm/keeper/query_plugins_test.go b/x/wasm/keeper/query_plugins_test.go index 05559e7ee8..1fea83a25f 100644 --- a/x/wasm/keeper/query_plugins_test.go +++ b/x/wasm/keeper/query_plugins_test.go @@ -576,7 +576,7 @@ func TestAcceptListStargateQuerier(t *testing.T) { require.NoError(t, err) addrs := app.AddTestAddrsIncremental(wasmApp, ctx, 2, sdkmath.NewInt(1_000_000)) - accepted := keeper.AcceptedStargateQueries{ + accepted := keeper.AcceptedQueries{ "/cosmos.auth.v1beta1.Query/Account": &authtypes.QueryAccountResponse{}, "/no/route/to/this": &authtypes.QueryAccountResponse{}, }