Skip to main content

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 oneofs.

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 AppModules 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 directly 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 methodQueryConnthat returns aClientConn` 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

Neutral​

References​