diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdfcb3388a..4d286fed189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#1667](https://github.com/osmosis-labs/osmosis/pull/1673) Move wasm-bindings code out of app package into its own root level package. * [#2013](https://github.com/osmosis-labs/osmosis/pull/2013) Make `SetParams`, `SetPool`, `SetTotalLiquidity`, and `SetDenomLiquidity` GAMM APIs private * [#1857](https://github.com/osmosis-labs/osmosis/pull/1857) x/mint rename GetLastHalvenEpochNum to GetLastReductionEpochNum +* [#2353](https://github.com/osmosis-labs/osmosis/pull/2353) Re-enable stargate query via whitelsit * [#2394](https://github.com/osmosis-labs/osmosis/pull/2394) Remove unused interface methods from expected keepers of each module * [#2390](https://github.com/osmosis-labs/osmosis/pull/2390) x/mint remove unused mintCoins parameter from AfterDistributeMintedCoin * [#2418](https://github.com/osmosis-labs/osmosis/pull/2418) x/mint remove SetInitialSupplyOffsetDuringMigration from keeper diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 1bf696fa4e5..b81b56f7b84 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -325,6 +325,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( supportedFeatures := "iterator,staking,stargate,osmosis" wasmOpts = append(owasm.RegisterCustomPlugins(appKeepers.GAMMKeeper, appKeepers.BankKeeper, appKeepers.TwapKeeper, appKeepers.TokenFactoryKeeper), wasmOpts...) + wasmOpts = append(owasm.RegisterStargateQueries(*bApp.GRPCQueryRouter(), appCodec), wasmOpts...) wasmKeeper := wasm.NewKeeper( appCodec, diff --git a/wasmbinding/query_plugin.go b/wasmbinding/query_plugin.go index df9bbbc6e71..4d18083a525 100644 --- a/wasmbinding/query_plugin.go +++ b/wasmbinding/query_plugin.go @@ -5,12 +5,45 @@ import ( "fmt" wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" "github.com/osmosis-labs/osmosis/v11/wasmbinding/bindings" ) +// StargateQuerier dispatches whitelisted stargate queries +func StargateQuerier(queryRouter baseapp.GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + protoResponse, whitelisted := StargateWhitelist.Load(request.Path) + if !whitelisted { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)} + } + + route := queryRouter.Route(request.Path) + if route == nil { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)} + } + + res, err := route(ctx, abci.RequestQuery{ + Data: request.Data, + Path: request.Path, + }) + if err != nil { + return nil, err + } + + bz, err := ConvertProtoToJSONMarshal(protoResponse, res.Value, codec) + if err != nil { + return nil, err + } + + return bz, nil + } +} + // CustomQuerier dispatches custom CosmWasm bindings queries. func CustomQuerier(qp *QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { @@ -138,6 +171,50 @@ func CustomQuerier(qp *QueryPlugin) func(ctx sdk.Context, request json.RawMessag } } +// ConvertProtoToJsonMarshal unmarshals the given bytes into a proto message and then marshals it to json. +// This is done so that clients calling stargate queries do not need to define their own proto unmarshalers, +// being able to use response directly by json marshalling, which is supported in cosmwasm. +// func ConvertProtoToJSONMarshal(protoResponse interface{}, bz []byte, cdc codec.Codec) ([]byte, error) { +// // all values are proto message +// message, ok := protoResponse.(proto.Message) +// if !ok { +// return nil, wasmvmtypes.Unknown{} +// } + +// // unmarshal binary into stargate response data structure +// err := proto.Unmarshal(bz, message) +// if err != nil { +// return nil, wasmvmtypes.Unknown{} +// } + +// bz, err = codec.MarshalJSON(message) +// if err != nil { +// return nil, wasmvmtypes.Unknown{} +// } + +// return bz, nil +// } +func ConvertProtoToJSONMarshal(protoResponse interface{}, bz []byte, cdc codec.Codec) ([]byte, error) { + // all values are proto message + message, ok := protoResponse.(codec.ProtoMarshaler) + if !ok { + return nil, wasmvmtypes.Unknown{} + } + + // unmarshal binary into stargate response data structure + err := cdc.Unmarshal(bz, message) + if err != nil { + return nil, wasmvmtypes.Unknown{} + } + + bz, err = cdc.MarshalJSON(message) + if err != nil { + return nil, wasmvmtypes.Unknown{} + } + + return bz, nil +} + // ConvertSdkCoinsToWasmCoins converts sdk type coins to wasm vm type coins func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmvmtypes.Coins { var toSend wasmvmtypes.Coins diff --git a/wasmbinding/query_plugin_test.go b/wasmbinding/query_plugin_test.go new file mode 100644 index 00000000000..0ff7d884644 --- /dev/null +++ b/wasmbinding/query_plugin_test.go @@ -0,0 +1,294 @@ +package wasmbinding_test + +import ( + "encoding/hex" + "fmt" + "testing" + "time" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + proto "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/suite" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + "google.golang.org/protobuf/runtime/protoiface" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/osmosis-labs/osmosis/v11/app" + epochtypes "github.com/osmosis-labs/osmosis/v11/x/epochs/types" + + "github.com/osmosis-labs/osmosis/v11/wasmbinding" +) + +type StargateTestSuite struct { + suite.Suite + + ctx sdk.Context + app *app.OsmosisApp +} + +func (suite *StargateTestSuite) SetupTest() { + suite.app = app.Setup(false) + suite.ctx = suite.app.BaseApp.NewContext(false, tmproto.Header{Height: 1, ChainID: "osmosis-1", Time: time.Now().UTC()}) +} + +func TestStargateTestSuite(t *testing.T) { + suite.Run(t, new(StargateTestSuite)) +} + +func (suite *StargateTestSuite) TestStargateQuerier() { + testCases := []struct { + name string + testSetup func() + path string + requestData func() []byte + responseProtoStruct interface{} + expectedQuerierError bool + expectedUnMarshalError bool + }{ + { + name: "happy path", + path: "/osmosis.epochs.v1beta1.Query/EpochInfos", + requestData: func() []byte { + epochrequest := epochtypes.QueryEpochsInfoRequest{} + bz, err := proto.Marshal(&epochrequest) + suite.Require().NoError(err) + return bz + }, + responseProtoStruct: &epochtypes.QueryEpochsInfoResponse{}, + }, + { + name: "unregistered path(not whitelisted)", + path: "/osmosis.epochs.v1beta1.Query/CurrentEpoch", + requestData: func() []byte { + currentEpochRequest := epochtypes.QueryCurrentEpochRequest{} + bz, err := proto.Marshal(¤tEpochRequest) + suite.Require().NoError(err) + return bz + }, + expectedQuerierError: true, + }, + { + name: "invalid query router route", + testSetup: func() { + wasmbinding.StargateWhitelist.Store("invalid/query/router/route", epochtypes.QueryEpochsInfoRequest{}) + }, + path: "invalid/query/router/route", + requestData: func() []byte { + return []byte{} + }, + expectedQuerierError: true, + }, + { + name: "unmatching path and data in request", + path: "/osmosis.epochs.v1beta1.Query/EpochInfos", + requestData: func() []byte { + epochrequest := epochtypes.QueryCurrentEpochRequest{} + bz, err := proto.Marshal(&epochrequest) + suite.Require().NoError(err) + return bz + }, + responseProtoStruct: &epochtypes.QueryCurrentEpochResponse{}, + expectedUnMarshalError: true, + }, + { + name: "error in unmarshalling response", + // set up whitelist with wrong data + testSetup: func() { + wasmbinding.StargateWhitelist.Store("/osmosis.epochs.v1beta1.Query/EpochInfos", interface{}(nil)) + }, + path: "/osmosis.epochs.v1beta1.Query/EpochInfos", + requestData: func() []byte { + return []byte{} + }, + responseProtoStruct: &epochtypes.QueryCurrentEpochResponse{}, + expectedQuerierError: true, + }, + { + name: "error in grpc querier", + // set up whitelist with wrong data + testSetup: func() { + wasmbinding.StargateWhitelist.Store("/cosmos.bank.v1beta1.Query/AllBalances", banktypes.QueryAllBalancesRequest{}) + }, + path: "/cosmos.bank.v1beta1.Query/AllBalances", + requestData: func() []byte { + bankrequest := banktypes.QueryAllBalancesRequest{} + bz, err := proto.Marshal(&bankrequest) + suite.Require().NoError(err) + return bz + }, + responseProtoStruct: &banktypes.QueryAllBalancesRequest{}, + expectedQuerierError: true, + }, + + // TODO: errors in wrong query in state machine + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + if tc.testSetup != nil { + tc.testSetup() + } + + stargateQuerier := wasmbinding.StargateQuerier(*suite.app.GRPCQueryRouter(), suite.app.AppCodec()) + stargateRequest := &wasmvmtypes.StargateQuery{ + Path: tc.path, + Data: tc.requestData(), + } + stargateResponse, err := stargateQuerier(suite.ctx, stargateRequest) + if tc.expectedQuerierError { + suite.Require().Error(err) + return + } else { + suite.Require().NoError(err) + + protoResponse, ok := tc.responseProtoStruct.(proto.Message) + suite.Require().True(ok) + + // test correctness by unmarshalling json response into proto struct + err = suite.app.AppCodec().UnmarshalJSON(stargateResponse, protoResponse) + if tc.expectedUnMarshalError { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + suite.Require().NotNil(protoResponse) + } + } + }) + } +} + +func (suite *StargateTestSuite) TestConvertProtoToJsonMarshal() { + testCases := []struct { + name string + queryPath string + protoResponseStruct proto.Message + originalResponse string + expectedProtoResponse proto.Message + expectedError bool + }{ + { + name: "successful conversion from proto response to json marshalled response", + queryPath: "/cosmos.bank.v1beta1.Query/AllBalances", + originalResponse: "0a090a036261721202333012050a03666f6f", + protoResponseStruct: &banktypes.QueryAllBalancesResponse{}, + expectedProtoResponse: &banktypes.QueryAllBalancesResponse{ + Balances: sdk.NewCoins(sdk.NewCoin("bar", sdk.NewInt(30))), + Pagination: &query.PageResponse{ + NextKey: []byte("foo"), + }, + }, + }, + { + name: "invalid proto response struct", + queryPath: "/cosmos.bank.v1beta1.Query/AllBalances", + originalResponse: "0a090a036261721202333012050a03666f6f", + protoResponseStruct: protoiface.MessageV1(nil), + expectedError: true, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + + originalVersionBz, err := hex.DecodeString(tc.originalResponse) + suite.Require().NoError(err) + + jsonMarshalledResponse, err := wasmbinding.ConvertProtoToJSONMarshal(tc.protoResponseStruct, originalVersionBz, suite.app.AppCodec()) + if tc.expectedError { + suite.Require().Error(err) + return + } + suite.Require().NoError(err) + + // check response by json marshalling proto response into json response manually + jsonMarshalExpectedResponse, err := suite.app.AppCodec().MarshalJSON(tc.expectedProtoResponse) + suite.Require().NoError(err) + suite.Require().Equal(jsonMarshalledResponse, jsonMarshalExpectedResponse) + }) + } +} + +// TestDeterministicJsonMarshal tests that we get deterministic JSON marshalled response upon +// proto struct update in the state machine. +func (suite *StargateTestSuite) TestDeterministicJsonMarshal() { + testCases := []struct { + name string + originalResponse string + updatedResponse string + queryPath string + responseProtoStruct interface{} + expectedProto func() proto.Message + }{ + /** + * + * Origin Response + * 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331346c3268686a6e676c3939367772703935673867646a6871653038326375367a7732706c686b + * + * Updated Response + * 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271122d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271 + // Origin proto + message QueryAccountResponse { + // account defines the account of the corresponding address. + google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"]; + } + // Updated proto + message QueryAccountResponse { + // account defines the account of the corresponding address. + google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"]; + // address is the address to query for. + string address = 2; + } + */ + { + "Query Account", + "0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679", + "0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679122d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679", + "/cosmos.auth.v1beta1.Query/Account", + &authtypes.QueryAccountResponse{}, + func() proto.Message { + account := authtypes.BaseAccount{ + Address: "cosmos1f8uxultn8sqzhznrsz3q77xwaquhgrsg6jyvfy", + } + accountResponse, err := codectypes.NewAnyWithValue(&account) + suite.Require().NoError(err) + return &authtypes.QueryAccountResponse{ + Account: accountResponse, + } + }, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + + binding, ok := wasmbinding.StargateWhitelist.Load(tc.queryPath) + suite.Require().True(ok) + + originVersionBz, err := hex.DecodeString(tc.originalResponse) + suite.Require().NoError(err) + jsonMarshalledOriginalBz, err := wasmbinding.ConvertProtoToJSONMarshal(binding, originVersionBz, suite.app.AppCodec()) + suite.Require().NoError(err) + + newVersionBz, err := hex.DecodeString(tc.updatedResponse) + suite.Require().NoError(err) + jsonMarshalledUpdatedBz, err := wasmbinding.ConvertProtoToJSONMarshal(binding, newVersionBz, suite.app.AppCodec()) + suite.Require().NoError(err) + + // json marshalled bytes should be the same since we use the same proto struct for unmarshalling + suite.Require().Equal(jsonMarshalledOriginalBz, jsonMarshalledUpdatedBz) + + // raw build also make same result + jsonMarshalExpectedResponse, err := suite.app.AppCodec().MarshalJSON(tc.expectedProto()) + suite.Require().NoError(err) + suite.Require().Equal(jsonMarshalledUpdatedBz, jsonMarshalExpectedResponse) + }) + } +} diff --git a/wasmbinding/stargate_whitelist.go b/wasmbinding/stargate_whitelist.go new file mode 100644 index 00000000000..f6c5f68deb6 --- /dev/null +++ b/wasmbinding/stargate_whitelist.go @@ -0,0 +1,24 @@ +package wasmbinding + +import ( + "sync" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + epochtypes "github.com/osmosis-labs/osmosis/v11/x/epochs/types" +) + +// StargateWhitelist keeps whitelist and its deterministic +// response binding for stargate queries. +// +// The query can be multi-thread, so we have to use +// thread safe sync.Map. +var StargateWhitelist sync.Map + +func init() { + StargateWhitelist.Store("/cosmos.auth.v1beta1.Query/Account", &authtypes.QueryAccountResponse{}) + StargateWhitelist.Store("/cosmos.bank.v1beta1.Query/AllBalances", &banktypes.QueryAllBalancesResponse{}) + + StargateWhitelist.Store("/osmosis.epochs.v1beta1.Query/EpochInfos", &epochtypes.QueryEpochsInfoResponse{}) +} diff --git a/wasmbinding/wasm.go b/wasmbinding/wasm.go index d5493a74852..53769158cdf 100644 --- a/wasmbinding/wasm.go +++ b/wasmbinding/wasm.go @@ -2,7 +2,11 @@ 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" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" gammkeeper "github.com/osmosis-labs/osmosis/v11/x/gamm/keeper" @@ -30,3 +34,13 @@ func RegisterCustomPlugins( messengerDecoratorOpt, } } + +func RegisterStargateQueries(queryRouter baseapp.GRPCQueryRouter, codec codec.Codec) []wasmkeeper.Option { + queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Stargate: StargateQuerier(queryRouter, codec), + }) + + return []wasm.Option{ + queryPluginOpt, + } +}