ADR 021: Protocol Buffer Query Encoding
Changelog
- 2020 March 27: Initial Draft
Status
Accepted
Context
This ADR is a continuation of the motivation, design, and context established in ADR 019 and ADR 020, namely, we aim to design the Protocol Buffer migration path for the client-side of the Cosmos SDK.
This ADR continues from ADD 020 to specify the encoding of queries.
Decision
Custom Query Definition
Modules define custom queries through a protocol buffers service
definition.
These service
definitions are generally associated with and used by the
GRPC protocol. However, the protocol buffers specification indicates that
they can be used more generically by any request/response protocol that uses
protocol buffer encoding. Thus, we can use service
definitions for specifying
custom ABCI queries and even reuse a substantial amount of the GRPC infrastructure.
Each module with custom queries should define a service canonically named Query
:
// x/bank/types/types.proto
service Query {
rpc QueryBalance(QueryBalanceParams) returns (cosmos_sdk.v1.Coin) { }
rpc QueryAllBalances(QueryAllBalancesParams) returns (QueryAllBalancesResponse) { }
}
Handling of Interface Types
Modules that use interface types and need true polymorphism generally force a
oneof
up to the app-level that provides the set of concrete implementations of
that interface that the app supports. While app's are welcome to do the same for
queries and implement an app-level query service, it is recommended that modules
provide query methods that expose these interfaces via google.protobuf.Any
.
There is a concern on the transaction level that the overhead of Any
is too
high to justify its usage. However for queries this is not a concern, and
providing generic module-level queries that use Any
does not preclude apps
from also providing app-level queries that return use the app-level oneof
s.
A hypothetical example for the gov
module would look something like:
// x/gov/types/types.proto
import "google/protobuf/any.proto";
service Query {
rpc GetProposal(GetProposalParams) returns (AnyProposal) { }
}
message AnyProposal {
ProposalBase base = 1;
google.protobuf.Any content = 2;
}
Custom Query Implementation
In order to implement the query service, we can reuse the existing gogo protobuf
grpc plugin, which for a service named Query
generates an interface named
QueryServer
as below:
type QueryServer interface {
QueryBalance(context.Context, *QueryBalanceParams) (*types.Coin, error)
QueryAllBalances(context.Context, *QueryAllBalancesParams) (*QueryAllBalancesResponse, error)
}
The custom queries for our module are implemented by implementing this interface.
The first parameter in this generated interface is a generic context.Context
,
whereas querier methods generally need an instance of sdk.Context
to read
from the store. Since arbitrary values can be attached to context.Context
using the WithValue
and Value
methods, the Cosmos SDK should provide a function
sdk.UnwrapSDKContext
to retrieve the sdk.Context
from the provided
context.Context
.
An example implementation of QueryBalance
for the bank module as above would
look something like:
type Querier struct {
Keeper
}
func (q Querier) QueryBalance(ctx context.Context, params *types.QueryBalanceParams) (*sdk.Coin, error) {
balance := q.GetBalance(sdk.UnwrapSDKContext(ctx), params.Address, params.Denom)
return &balance, nil
}
Custom Query Registration and Routing
Query server implementations as above would be registered with AppModule
s using
a new method RegisterQueryService(grpc.Server)
which could be implemented simply
as below:
// x/bank/module.go
func (am AppModule) RegisterQueryService(server grpc.Server) {
types.RegisterQueryServer(server, keeper.Querier{am.keeper})
}
Underneath the hood, a new method RegisterService(sd *grpc.ServiceDesc, handler interface{})
will be added to the existing baseapp.QueryRouter
to add the queries to the custom
query routing table (with the routing method being described below).
The signature for this method matches the existing
RegisterServer
method on the GRPC Server
type where handler
is the custom
query server implementation described above.
GRPC-like requests are routed by the service name (ex. cosmos_sdk.x.bank.v1.Query
)
and method name (ex. QueryBalance
) combined with /
s to form a full
method name (ex. /cosmos_sdk.x.bank.v1.Query/QueryBalance
). This gets translated
into an ABCI query as custom/cosmos_sdk.x.bank.v1.Query/QueryBalance
. Service handlers
registered with QueryRouter.RegisterService
will be routed this way.
Beyond the method name, GRPC requests carry a protobuf encoded payload, which maps naturally
to RequestQuery.Data
, and receive a protobuf encoded response or error. Thus
there is a quite natural mapping of GRPC-like rpc methods to the existing
sdk.Query
and QueryRouter
infrastructure.
This basic specification allows us to reuse protocol buffer service
definitions
for ABCI custom queries substantially reducing the need for manual decoding and
encoding in query methods.
GRPC Protocol Support
In addition to providing an ABCI query pathway, we can easily provide a GRPC
proxy server that routes requests in the GRPC protocol to ABCI query requests
under the hood. In this way, clients could use their host languages' existing
GRPC implementations to make direct queries against Cosmos SDK app's using
these service
definitions. In order for this server to work, the QueryRouter
on BaseApp
will need to expose the service handlers registered with
QueryRouter.RegisterService
to the proxy server implementation. Nodes could
launch the proxy server on a separate port in the same process as the ABCI app
with a command-line flag.
REST Queries and Swagger Generation
grpc-gateway is a project that
translates REST calls into GRPC calls using special annotations on service
methods. Modules that want to expose REST queries should add google.api.http
annotations to their rpc
methods as in this example below.
// x/bank/types/types.proto
service Query {
rpc QueryBalance(QueryBalanceParams) returns (cosmos_sdk.v1.Coin) {
option (google.api.http) = {
get: "/x/bank/v1/balance/{address}/{denom}"
};
}
rpc QueryAllBalances(QueryAllBalancesParams) returns (QueryAllBalancesResponse) {
option (google.api.http) = {
get: "/x/bank/v1/balances/{address}"
};
}
}
grpc-gateway will work direcly against the GRPC proxy described above which will translate requests to ABCI queries under the hood. grpc-gateway can also generate Swagger definitions automatically.
In the current implementation of REST queries, each module needs to implement REST queries manually in addition to ABCI querier methods. Using the grpc-gateway approach, there will be no need to generate separate REST query handlers, just query servers as described above as grpc-gateway handles the translation of protobuf to REST as well as Swagger definitions.
The Cosmos SDK should provide CLI commands for apps to start GRPC gateway either in
a separate process or the same process as the ABCI app, as well as provide a
command for generating grpc-gateway proxy .proto
files and the swagger.json
file.
Client Usage
The gogo protobuf grpc plugin generates client interfaces in addition to server
interfaces. For the Query
service defined above we would get a QueryClient
interface like:
type QueryClient interface {
QueryBalance(ctx context.Context, in *QueryBalanceParams, opts ...grpc.CallOption) (*types.Coin, error)
QueryAllBalances(ctx context.Context, in *QueryAllBalancesParams, opts ...grpc.CallOption) (*QueryAllBalancesResponse, error)
}
Via a small patch to gogo protobuf (gogo/protobuf#675) we have tweaked the grpc codegen to use an interface rather than concrete type for the generated client struct. This allows us to also reuse the GRPC infrastructure for ABCI client queries.
1Contextwill receive a new method
QueryConnthat returns a
ClientConn`
that routes calls to ABCI queries
Clients (such as CLI methods) will then be able to call query methods like this:
clientCtx := client.NewContext()
queryClient := types.NewQueryClient(clientCtx.QueryConn())
params := &types.QueryBalanceParams{addr, denom}
result, err := queryClient.QueryBalance(gocontext.Background(), params)
Testing
Tests would be able to create a query client directly from keeper and sdk.Context
references using a QueryServerTestHelper
as below:
queryHelper := baseapp.NewQueryServerTestHelper(ctx)
types.RegisterQueryServer(queryHelper, keeper.Querier{app.BankKeeper})
queryClient := types.NewQueryClient(queryHelper)
Future Improvements
Consequences
Positive
- greatly simplified querier implementation (no manual encoding/decoding)
- easy query client generation (can use existing grpc and swagger tools)
- no need for REST query implementations
- type safe query methods (generated via grpc plugin)
- going forward, there will be less breakage of query methods because of the backwards compatibility guarantees provided by buf
Negative
- all clients using the existing ABCI/REST queries will need to be refactored for both the new GRPC/REST query paths as well as protobuf/proto-json encoded data, but this is more or less unavoidable in the protobuf refactoring