Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wasmbinding)!: whitelisted stargate queries for QueryRequest::Stargate: auth, bank, gov, tokenfactory, epochs, inflation, oracle, sudo, devgas #1646

Merged
merged 12 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#1616](https://github.com/NibiruChain/nibiru/pull/1616) - fix(app)!: Add custom wasm snapshotter for proper state exports
* [#1617](https://github.com/NibiruChain/nibiru/pull/1617) - fix(app)!: non-nil snapshot manager is not guaranteed in testapp
* [#1645](https://github.com/NibiruChain/nibiru/pull/1645) - fix(tokenfactory)!: token supply in bank keeper must be correct after MsgBurn.
* [#1646](https://github.com/NibiruChain/nibiru/pull/1646) - feat(wasmbinding)!: whitelisted stargate queries for QueryRequest::Stargate: auth, bank, gov, tokenfactory, epochs, inflation, oracle, sudo, devgas

### Improvements

Expand Down
4 changes: 3 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasm.Option
}

// Add the bindings to the app's set of []wasm.Option.
wasmOpts = append(wasmOpts, wasmbinding.RegisterWasmOptions(
wasmOpts = append(wasmOpts, wasmbinding.NibiruWasmOptions(
nibiru.GRPCQueryRouter(),
nibiru.appCodec,
nibiru.PerpKeeperV2,
nibiru.SudoKeeper,
nibiru.OracleKeeper,
Expand Down
12 changes: 6 additions & 6 deletions app/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ type AppKeepers struct {
the app, so we can SetRouter on it correctly. */
ibcKeeper *ibckeeper.Keeper
ibcFeeKeeper ibcfeekeeper.Keeper
/* transferKeeper is for cross-chain fungible token transfers. */
transferKeeper ibctransferkeeper.Keeper
/* ibcTransferKeeper is for cross-chain fungible token transfers. */
ibcTransferKeeper ibctransferkeeper.Keeper

// make scoped keepers public for test purposes
ScopedIBCKeeper capabilitykeeper.ScopedKeeper
Expand Down Expand Up @@ -434,7 +434,7 @@ func (app *NibiruApp) InitKeepers(
app.ibcKeeper.ChannelKeeper,
&app.ibcKeeper.PortKeeper,
app.ScopedWasmKeeper,
app.transferKeeper,
app.ibcTransferKeeper,
app.MsgServiceRouter(),
app.GRPCQueryRouter(),
wasmDir,
Expand Down Expand Up @@ -478,7 +478,7 @@ func (app *NibiruApp) InitKeepers(
/* Create IBC module and a static IBC router */
ibcRouter := porttypes.NewRouter()

app.transferKeeper = ibctransferkeeper.NewKeeper(
app.ibcTransferKeeper = ibctransferkeeper.NewKeeper(
appCodec,
keys[ibctransfertypes.StoreKey],
/* paramSubspace */ app.GetSubspace(ibctransfertypes.ModuleName),
Expand Down Expand Up @@ -508,7 +508,7 @@ func (app *NibiruApp) InitKeepers(

// create IBC module from bottom to top of stack
var transferStack porttypes.IBCModule
transferStack = ibctransfer.NewIBCModule(app.transferKeeper)
transferStack = ibctransfer.NewIBCModule(app.ibcTransferKeeper)
transferStack = ibcfee.NewIBCMiddleware(transferStack, app.ibcFeeKeeper)

// Add transfer stack to IBC Router
Expand Down Expand Up @@ -599,7 +599,7 @@ func (app *NibiruApp) initAppModules(
// ibc
evidence.NewAppModule(app.evidenceKeeper),
ibc.NewAppModule(app.ibcKeeper),
ibctransfer.NewAppModule(app.transferKeeper),
ibctransfer.NewAppModule(app.ibcTransferKeeper),
ibcfee.NewAppModule(app.ibcFeeKeeper),

// wasm
Expand Down
6 changes: 3 additions & 3 deletions proto/nibiru/epochs/v1/query.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
syntax = "proto3";

Check failure on line 1 in proto/nibiru/epochs/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present message "QueryEpochsInfoRequest" was deleted from file.

Check failure on line 1 in proto/nibiru/epochs/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present message "QueryEpochsInfoResponse" was deleted from file.
package nibiru.epochs.v1;

import "gogoproto/gogo.proto";
Expand All @@ -11,7 +11,7 @@
// Query defines the gRPC querier service.
service Query {
// EpochInfos provide running epochInfos
rpc EpochInfos(QueryEpochsInfoRequest) returns (QueryEpochsInfoResponse) {
rpc EpochInfos(QueryEpochInfosRequest) returns (QueryEpochInfosResponse) {

Check failure on line 14 in proto/nibiru/epochs/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

RPC "EpochInfos" on service "Query" changed request type from "nibiru.epochs.v1.QueryEpochsInfoRequest" to "nibiru.epochs.v1.QueryEpochInfosRequest".

Check failure on line 14 in proto/nibiru/epochs/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

RPC "EpochInfos" on service "Query" changed response type from "nibiru.epochs.v1.QueryEpochsInfoResponse" to "nibiru.epochs.v1.QueryEpochInfosResponse".
option (google.api.http).get = "/nibiru/epochs/v1beta1/epochs";
}
// CurrentEpoch provide current epoch of specified identifier
Expand All @@ -21,8 +21,8 @@
}
}

message QueryEpochsInfoRequest {}
message QueryEpochsInfoResponse {
message QueryEpochInfosRequest {}
message QueryEpochInfosResponse {
repeated nibiru.epochs.v1.EpochInfo epochs = 1
[ (gogoproto.nullable) = false ];
}
Expand Down
150 changes: 150 additions & 0 deletions wasmbinding/stargate_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package wasmbinding

import (
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"

devgas "github.com/NibiruChain/nibiru/x/devgas/v1/types"
epochs "github.com/NibiruChain/nibiru/x/epochs/types"
inflation "github.com/NibiruChain/nibiru/x/inflation/types"
oracle "github.com/NibiruChain/nibiru/x/oracle/types"
sudotypes "github.com/NibiruChain/nibiru/x/sudo/types"
tokenfactory "github.com/NibiruChain/nibiru/x/tokenfactory/types"

auth "github.com/cosmos/cosmos-sdk/x/auth/types"
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1"

ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
ibcconnectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types"
)

/*
WasmAcceptedStargateQueries: Specifies which `QueryRequest::Stargate` types
can be sent to the application.

### On Stargate Queries:

A Stargate query is encoded the same way as abci_query, with path and protobuf
encoded request data. The format is defined in
[ADR-21](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-021-protobuf-query-encoding.md).
- The response is protobuf encoded data directly without a JSON response wrapper.
The caller is responsible for compiling the proper protobuf definitions for both
requests and responses.

```rust
enum QueryRequest {
Stargate {
/// this is the fully qualified service path used for routing,
/// eg. custom/cosmos_sdk.x.bank.v1.Query/QueryBalance
path: String,
/// this is the expected protobuf message type (not any), binary encoded
data: Binary,
},
// ...
}
```

### Relationship with Protobuf Message:

A protobuf message with type URL "/cosmos.bank.v1beta1.QueryBalanceResponse"
communicates a lot of information. From this type URL, we know:
- The protobuf message has package "cosmos.bank.v1beta1"
- The protobuf message has name "QueryBalanceResponse"

That is, a type URL is of the form "/[PB_MSG.PACKAGE]/[PB_MSG.NAME]"

The `QueryRequest::Stargate.path` is defined based on method name of the gRPC
service description, not the type URL. In this example:
- The service name is "cosmos.bank.v1beta1.Query"
- The method name for this request on that service is "Balance"

This results in the expected `Stargate.path` of "/[SERVICE_NAME]/[METHOD]".
By convention, the gRPC query service corresponding to a package is always
"[PB_MSG.PACKAGE].Query".

Given only the `PB_MSG.PACKAGE` and the `PB_MSG.NAME` of either the query
request or response, we should know the `QueryRequest::Stargate.path`
deterministically.
*/
func WasmAcceptedStargateQueries() wasmkeeper.AcceptedStargateQueries {
return wasmkeeper.AcceptedStargateQueries{
// ibc
"/ibc.core.client.v1.Query/ClientState": &ibcclienttypes.QueryClientStateResponse{},
"/ibc.core.client.v1.Query/ConsensusState": &ibcclienttypes.QueryConsensusStateResponse{},
"/ibc.core.connection.v1.Query/Connection": &ibcconnectiontypes.QueryConnectionResponse{},
"/ibc.core.connection.v1.Query/Connections": &ibcconnectiontypes.QueryConnectionsResponse{},
"/ibc.core.connection.v1.Query/ClientConnections": &ibcconnectiontypes.QueryClientConnectionsResponse{},
"/ibc.core.connection.v1.Query/ConnectionConsensusState": &ibcconnectiontypes.QueryConnectionConsensusStateResponse{},
"/ibc.core.connection.v1.Query/ConnectionParams": &ibcconnectiontypes.QueryConnectionParamsResponse{},

// ibc transfer
"/ibc.applications.transfer.v1.Query/DenomTrace": &ibctransfertypes.QueryDenomTraceResponse{},
"/ibc.applications.transfer.v1.Query/Params": &ibctransfertypes.QueryParamsResponse{},
"/ibc.applications.transfer.v1.Query/DenomHash": &ibctransfertypes.QueryDenomHashResponse{},
"/ibc.applications.transfer.v1.Query/EscrowAddress": &ibctransfertypes.QueryEscrowAddressResponse{},
"/ibc.applications.transfer.v1.Query/TotalEscrowForDenom": &ibctransfertypes.QueryTotalEscrowForDenomResponse{},

// cosmos auth
"/cosmos.auth.v1beta1.Query/Account": new(auth.QueryAccountResponse),
"/cosmos.auth.v1beta1.Query/Params": new(auth.QueryParamsResponse),

// cosmos bank
"/cosmos.bank.v1beta1.Query/Balance": new(bank.QueryBalanceResponse),
"/cosmos.bank.v1beta1.Query/DenomMetadata": new(bank.QueryDenomMetadataResponse),
"/cosmos.bank.v1beta1.Query/Params": new(bank.QueryParamsResponse),
"/cosmos.bank.v1beta1.Query/SupplyOf": new(bank.QuerySupplyOfResponse),
"/cosmos.bank.v1beta1.Query/AllBalances": new(bank.QueryAllBalancesResponse),

// cosmos gov
"/cosmos.gov.v1.Query/Proposal": new(gov.QueryProposalResponse),
"/cosmos.gov.v1.Query/Params": new(gov.QueryParamsResponse),
"/cosmos.gov.v1.Query/Vote": new(gov.QueryVoteResponse),

// nibiru tokenfactory
"/nibiru.tokenfactory.v1.Query/Denoms": new(tokenfactory.QueryDenomsResponse),
"/nibiru.tokenfactory.v1.Query/Params": new(tokenfactory.QueryParamsResponse),
"/nibiru.tokenfactory.v1.Query/DenomInfo": new(tokenfactory.QueryDenomInfoResponse),

// nibiru epochs
"/nibiru.epochs.v1.Query/EpochInfos": new(epochs.QueryEpochInfosResponse),
"/nibiru.epochs.v1.Query/CurrentEpoch": new(epochs.QueryCurrentEpochResponse),

// nibiru inflation
"/nibiru.inflation.v1.Query/Period": new(inflation.QueryPeriodResponse),
"/nibiru.inflation.v1.Query/EpochMintProvision": new(inflation.QueryEpochMintProvisionResponse),
"/nibiru.inflation.v1.Query/SkippedEpochs": new(inflation.QuerySkippedEpochsResponse),
"/nibiru.inflation.v1.Query/CirculatingSupply": new(inflation.QueryCirculatingSupplyResponse),
"/nibiru.inflation.v1.Query/InflationRate": new(inflation.QueryInflationRateResponse),
"/nibiru.inflation.v1.Query/Params": new(inflation.QueryParamsResponse),

// nibiru oracle
"/nibiru.oracle.v1.Query/ExchangeRate": new(oracle.QueryExchangeRateResponse),
"/nibiru.oracle.v1.Query/ExchangeRateTwap": new(oracle.QueryExchangeRateResponse),
"/nibiru.oracle.v1.Query/ExchangeRates": new(oracle.QueryExchangeRatesResponse),
"/nibiru.oracle.v1.Query/Actives": new(oracle.QueryActivesResponse),
"/nibiru.oracle.v1.Query/VoteTargets": new(oracle.QueryVoteTargetsResponse),
"/nibiru.oracle.v1.Query/FeederDelegation": new(oracle.QueryFeederDelegationResponse),
"/nibiru.oracle.v1.Query/MissCounter": new(oracle.QueryMissCounterResponse),
"/nibiru.oracle.v1.Query/AggregatePrevote": new(oracle.QueryAggregatePrevoteResponse),
"/nibiru.oracle.v1.Query/AggregatePrevotes": new(oracle.QueryAggregatePrevotesResponse),
"/nibiru.oracle.v1.Query/AggregateVote": new(oracle.QueryAggregateVoteResponse),
"/nibiru.oracle.v1.Query/AggregateVotes": new(oracle.QueryAggregateVotesResponse),
"/nibiru.oracle.v1.Query/Params": new(oracle.QueryParamsResponse),

// nibiru sudo
"/nibiru.sudo.v1.Query/QuerySudoers": new(sudotypes.QuerySudoersResponse),

// nibiru devgas
"/nibiru.devgas.v1.Query/FeeShares": new(devgas.QueryFeeSharesResponse),
"/nibiru.devgas.v1.Query/FeeShare": new(devgas.QueryFeeShareResponse),
"/nibiru.devgas.v1.Query/Params": new(devgas.QueryParamsResponse),
"/nibiru.devgas.v1.Query/FeeSharesByWithdrawer": new(devgas.QueryFeeSharesByWithdrawerResponse),

// TODO: for post v1
// nibiru.perp

// TODO: for post v1
// nibiru.spot
}
}
116 changes: 116 additions & 0 deletions wasmbinding/stargate_query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package wasmbinding_test

import (
"fmt"
"strings"
"testing"

"github.com/cosmos/gogoproto/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"

"github.com/NibiruChain/nibiru/wasmbinding"

"github.com/NibiruChain/nibiru/x/common/set"

devgas "github.com/NibiruChain/nibiru/x/devgas/v1/types"
epochs "github.com/NibiruChain/nibiru/x/epochs/types"
inflation "github.com/NibiruChain/nibiru/x/inflation/types"
oracle "github.com/NibiruChain/nibiru/x/oracle/types"
sudotypes "github.com/NibiruChain/nibiru/x/sudo/types"
tokenfactory "github.com/NibiruChain/nibiru/x/tokenfactory/types"
)

/*
TestWasmAcceptedStargateQueries: Verifies that the query paths registered in
the Wasm keeper's StargateQuerier are the official method names in the gRPC
query service of each path's respective module.

> ℹ️ "All stargate query paths must be actual GRPC query service methods"

Please see the function doc comment for WasmAcceptedStargateQueries in
stargate_query.go to understand in detail what invariants this test checks
for.

Given only the `PB_MSG.PACKAGE` and the `PB_MSG.NAME` of either the query
request or response, we should know the `QueryRequest::Stargate.path`
deterministically.
*/
func TestWasmAcceptedStargateQueries(t *testing.T) {
t.Log("stargateQueryPaths: Add nibiru query paths from GRPC service descriptions")
queryServiceDescriptions := []grpc.ServiceDesc{
epochs.GrpcQueryServiceDesc(),
devgas.GrpcQueryServiceDesc(),
inflation.GrpcQueryServiceDesc(),
oracle.GrpcQueryServiceDesc(),
sudotypes.GrpcQueryServiceDesc(),
tokenfactory.GrpcQueryServiceDesc(),
}
stargateQueryPaths := set.New[string]()
for _, serviceDesc := range queryServiceDescriptions {
for _, queryMethod := range serviceDesc.Methods {
stargateQueryPaths.Add(
fmt.Sprintf("/%v/%v", serviceDesc.ServiceName, queryMethod.MethodName),
)
}
}

t.Log("stargateQueryPaths: Add cosmos and ibc query paths")
// The GRPC service descriptions aren't exported as copies from the
// Cosmos-SDK and remain private vars. Maybe we could ask the maintainers to
// export them in the future.
for queryPath := range wasmbinding.WasmAcceptedStargateQueries() {
stargateQueryPaths.Add(queryPath)
}

// It's not required for the response type and the method description of the
// stargate query's gRPC path to match up exactly as expected. The exception
// to this convention is when our response type isn't stripped of its
// "Response" suffix and "Query" prefix is not the same as the method name.
// This happens when "QueryAAARequest" does not return a "QueryAAAResponse".
exceptionPaths := set.New[string]("/nibiru.oracle.v1.QueryExchangeRateResponse")

gotQueryPaths := []string{}
for queryPath, protobufResponse := range wasmbinding.WasmAcceptedStargateQueries() {
gotQueryPaths = append(gotQueryPaths, queryPath)

// Show that the underlying protobuf name and query paths coincide.
pbQueryResponseTypeUrl := "/" + proto.MessageName(protobufResponse)
isExceptionPath := exceptionPaths.Has(pbQueryResponseTypeUrl)
splitResponse := strings.Split(pbQueryResponseTypeUrl, "Response")
assert.Lenf(t, splitResponse, 2, "typeUrl: %v",
splitResponse, pbQueryResponseTypeUrl)

// Get proto message "package" from the response type
typeUrlMinusSuffix := splitResponse[0]
typeUrlPartsFromProtoMsg := strings.Split(typeUrlMinusSuffix, ".")
lenOfParts := len(typeUrlPartsFromProtoMsg)
assert.GreaterOrEqual(t, lenOfParts, 4, typeUrlPartsFromProtoMsg)
protoMessagePackage := typeUrlPartsFromProtoMsg[:lenOfParts-1]

// Get proto message "package" from the query path
typeUrlPartsFromQueryPath := strings.Split(queryPath, ".")
assert.GreaterOrEqual(t, len(typeUrlPartsFromQueryPath), 4, typeUrlPartsFromQueryPath)
queryPathProtoPackage := typeUrlPartsFromQueryPath[:lenOfParts-1]

// Verify that the packages match
assert.Equalf(t, queryPathProtoPackage, protoMessagePackage,
"package names inconsistent:\nfrom query path: %v\nfrom protobuf object: %v",
queryPath, pbQueryResponseTypeUrl,
)

// Verify that the method names match too.
if isExceptionPath {
continue
}
methodNameFromPb := strings.TrimLeft(typeUrlPartsFromProtoMsg[3], "Query")
methodNameFromPath := strings.TrimLeft(typeUrlPartsFromQueryPath[3], "Query/")
assert.Equalf(t, methodNameFromPb, methodNameFromPath,
"method names inconsistent:\nfrom query path: %v\nfrom protobuf object: %v",
queryPath, pbQueryResponseTypeUrl,
)
}

t.Log("All stargate query paths must be actual GRPC query service methods")
assert.ElementsMatch(t, stargateQueryPaths.ToSlice(), gotQueryPaths)
}
13 changes: 12 additions & 1 deletion wasmbinding/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,32 @@ package wasmbinding
import (
"github.com/CosmWasm/wasmd/x/wasm"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"

"github.com/NibiruChain/nibiru/x/sudo/keeper"

oraclekeeper "github.com/NibiruChain/nibiru/x/oracle/keeper"
perpv2keeper "github.com/NibiruChain/nibiru/x/perp/v2/keeper"
)

func RegisterWasmOptions(
// NibiruWasmOptions: Wasm Options are extension points to instantiate the Wasm
// keeper with non-default values
func NibiruWasmOptions(
grpcQueryRouter *baseapp.GRPCQueryRouter,
appCodec codec.Codec,
perpv2 perpv2keeper.Keeper,
sudoKeeper keeper.Keeper,
oracleKeeper oraclekeeper.Keeper,
) []wasm.Option {
wasmQueryPlugin := NewQueryPlugin(perpv2, oracleKeeper)
wasmQueryOption := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{
Custom: CustomQuerier(wasmQueryPlugin),
Stargate: wasmkeeper.AcceptListStargateQuerier(
WasmAcceptedStargateQueries(),
grpcQueryRouter,
appCodec,
),
})

wasmExecuteOption := wasmkeeper.WithMessageHandlerDecorator(
Expand Down
Loading
Loading