diff --git a/x/wasm/keeper/keeper.go b/x/wasm/keeper/keeper.go index e7b4c37ee6..83f0581f80 100644 --- a/x/wasm/keeper/keeper.go +++ b/x/wasm/keeper/keeper.go @@ -119,7 +119,7 @@ func NewKeeper( paramSpace: paramSpace, gasRegister: NewDefaultWasmGasRegister(), } - keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, keeper) + keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, keeper, cdc) for _, o := range opts { o.apply(keeper) } diff --git a/x/wasm/keeper/query_plugins.go b/x/wasm/keeper/query_plugins.go index b0e095b12c..5a3b383130 100644 --- a/x/wasm/keeper/query_plugins.go +++ b/x/wasm/keeper/query_plugins.go @@ -3,8 +3,11 @@ package keeper import ( "encoding/json" "errors" + "fmt" "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + abci "github.com/tendermint/tendermint/abci/types" channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" @@ -101,13 +104,14 @@ func DefaultQueryPlugins( channelKeeper types.ChannelKeeper, queryRouter GRPCQueryRouter, wasm wasmQueryKeeper, + codec codec.Codec, ) QueryPlugins { return QueryPlugins{ Bank: BankQuerier(bank), Custom: NoCustomQuerier, IBC: IBCQuerier(wasm, channelKeeper), Staking: StakingQuerier(staking, distKeeper), - Stargate: StargateQuerier(queryRouter), + Stargate: StargateQuerier(queryRouter, codec), Wasm: WasmQuerier(wasm), } } @@ -268,9 +272,32 @@ func IBCQuerier(wasm contractMetaDataSource, channelKeeper types.ChannelKeeper) } } -func StargateQuerier(queryRouter GRPCQueryRouter) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { - return func(ctx sdk.Context, msg *wasmvmtypes.StargateQuery) ([]byte, error) { - return nil, wasmvmtypes.UnsupportedRequest{Kind: "Stargate queries are disabled."} +func StargateQuerier(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, whitelisted := AcceptList.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 } } @@ -517,6 +544,30 @@ func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin { } } +// 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.(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 +} + var _ WasmVMQueryHandler = WasmVMQueryHandlerFn(nil) // WasmVMQueryHandlerFn is a helper to construct a function based query handler. diff --git a/x/wasm/keeper/query_plugins_test.go b/x/wasm/keeper/query_plugins_test.go index c58932d67e..c6fdbc1c09 100644 --- a/x/wasm/keeper/query_plugins_test.go +++ b/x/wasm/keeper/query_plugins_test.go @@ -1,19 +1,30 @@ -package keeper +package keeper_test import ( + "encoding/hex" "encoding/json" + "fmt" "testing" + "github.com/CosmWasm/wasmd/app" + "google.golang.org/protobuf/runtime/protoiface" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/store" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/golang/protobuf/proto" dbm "github.com/tendermint/tm-db" wasmvmtypes "github.com/CosmWasm/wasmvm/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/CosmWasm/wasmd/x/wasm/keeper" "github.com/CosmWasm/wasmd/x/wasm/keeper/wasmtesting" "github.com/CosmWasm/wasmd/x/wasm/types" ) @@ -307,8 +318,8 @@ func TestIBCQuerier(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { - h := IBCQuerier(spec.wasmKeeper, spec.channelKeeper) - gotResult, gotErr := h(sdk.Context{}, RandomAccountAddress(t), spec.srcQuery) + h := keeper.IBCQuerier(spec.wasmKeeper, spec.channelKeeper) + gotResult, gotErr := h(sdk.Context{}, keeper.RandomAccountAddress(t), spec.srcQuery) require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr) if spec.expErr != nil { return @@ -324,10 +335,10 @@ func TestBankQuerierBalance(t *testing.T) { }} ctx := sdk.Context{} - q := BankQuerier(mock) + q := keeper.BankQuerier(mock) gotBz, gotErr := q(ctx, &wasmvmtypes.BankQuery{ Balance: &wasmvmtypes.BalanceQuery{ - Address: RandomBech32AccountAddress(t), + Address: keeper.RandomBech32AccountAddress(t), Denom: "ALX", }, }) @@ -344,9 +355,9 @@ func TestBankQuerierBalance(t *testing.T) { } func TestContractInfoWasmQuerier(t *testing.T) { - myValidContractAddr := RandomBech32AccountAddress(t) - myCreatorAddr := RandomBech32AccountAddress(t) - myAdminAddr := RandomBech32AccountAddress(t) + myValidContractAddr := keeper.RandomBech32AccountAddress(t) + myCreatorAddr := keeper.RandomBech32AccountAddress(t) + myAdminAddr := keeper.RandomBech32AccountAddress(t) var ctx sdk.Context specs := map[string]struct { @@ -433,7 +444,7 @@ func TestContractInfoWasmQuerier(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { - q := WasmQuerier(spec.mock) + q := keeper.WasmQuerier(spec.mock) gotBz, gotErr := q(ctx, spec.req) if spec.expErr { require.Error(t, gotErr) @@ -464,11 +475,11 @@ func TestQueryErrors(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { - mock := WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + mock := keeper.WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { return nil, spec.src }) ctx := sdk.Context{}.WithGasMeter(sdk.NewInfiniteGasMeter()).WithMultiStore(store.NewCommitMultiStore(dbm.NewMemDB())) - q := NewQueryHandler(ctx, mock, sdk.AccAddress{}, NewDefaultWasmGasRegister()) + q := keeper.NewQueryHandler(ctx, mock, sdk.AccAddress{}, keeper.NewDefaultWasmGasRegister()) _, gotErr := q.Query(wasmvmtypes.QueryRequest{}, 1) assert.Equal(t, spec.expErr, gotErr) }) @@ -528,3 +539,131 @@ func (m bankKeeperMock) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk } return m.GetAllBalancesFn(ctx, addr) } + +func TestConvertProtoToJSONMarshal(t *testing.T) { + 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 { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + // set up app for testing + wasmApp := app.SetupWithEmptyStore(t) + + originalVersionBz, err := hex.DecodeString(tc.originalResponse) + require.NoError(t, err) + + jsonMarshalledResponse, err := keeper.ConvertProtoToJSONMarshal(tc.protoResponseStruct, originalVersionBz, wasmApp.AppCodec()) + if tc.expectedError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check response by json marshalling proto response into json response manually + jsonMarshalExpectedResponse, err := wasmApp.AppCodec().MarshalJSON(tc.expectedProtoResponse) + require.NoError(t, err) + require.JSONEq(t, string(jsonMarshalledResponse), string(jsonMarshalExpectedResponse)) + }) + } +} + +// TestDeterministicJsonMarshal tests that we get deterministic JSON marshalled response upon +// proto struct update in the state machine. +func TestDeterministicJsonMarshal(t *testing.T) { + 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) + require.NoError(t, err) + return &authtypes.QueryAccountResponse{ + Account: accountResponse, + } + }, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + wasmApp := app.SetupWithEmptyStore(t) + + originVersionBz, err := hex.DecodeString(tc.originalResponse) + require.NoError(t, err) + jsonMarshalledOriginalBz, err := keeper.ConvertProtoToJSONMarshal(tc.responseProtoStruct, originVersionBz, wasmApp.AppCodec()) + require.NoError(t, err) + + newVersionBz, err := hex.DecodeString(tc.updatedResponse) + require.NoError(t, err) + jsonMarshalledUpdatedBz, err := keeper.ConvertProtoToJSONMarshal(tc.responseProtoStruct, newVersionBz, wasmApp.AppCodec()) + require.NoError(t, err) + + // json marshalled bytes should be the same since we use the same proto struct for unmarshalling + require.Equal(t, jsonMarshalledOriginalBz, jsonMarshalledUpdatedBz) + + // raw build also make same result + jsonMarshalExpectedResponse, err := wasmApp.AppCodec().MarshalJSON(tc.expectedProto()) + require.NoError(t, err) + require.Equal(t, jsonMarshalledUpdatedBz, jsonMarshalExpectedResponse) + }) + } +} diff --git a/x/wasm/keeper/reflect_test.go b/x/wasm/keeper/reflect_test.go index 1c761ef984..6b390bc0e7 100644 --- a/x/wasm/keeper/reflect_test.go +++ b/x/wasm/keeper/reflect_test.go @@ -395,10 +395,10 @@ func TestReflectInvalidStargateQuery(t *testing.T) { }) require.NoError(t, err) - // make a query on the chain, should be blacklisted + // make a query on the chain, should not be whitelisted _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) require.Error(t, err) - require.Contains(t, err.Error(), "Stargate queries are disabled") + require.Contains(t, err.Error(), "Unsupported query") // now, try to build a protobuf query protoRequest = wasmvmtypes.QueryRequest{ @@ -415,7 +415,7 @@ func TestReflectInvalidStargateQuery(t *testing.T) { // make a query on the chain, should be blacklisted _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) require.Error(t, err) - require.Contains(t, err.Error(), "Stargate queries are disabled") + require.Contains(t, err.Error(), "Unsupported query") // and another one protoRequest = wasmvmtypes.QueryRequest{ @@ -432,7 +432,7 @@ func TestReflectInvalidStargateQuery(t *testing.T) { // make a query on the chain, should be blacklisted _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) require.Error(t, err) - require.Contains(t, err.Error(), "Stargate queries are disabled") + require.Contains(t, err.Error(), "Unsupported query") } type reflectState struct { diff --git a/x/wasm/keeper/stargate_whitelist.go b/x/wasm/keeper/stargate_whitelist.go new file mode 100644 index 0000000000..ce90738035 --- /dev/null +++ b/x/wasm/keeper/stargate_whitelist.go @@ -0,0 +1,18 @@ +package keeper + +import ( + "sync" +) + +// AcceptList 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 AcceptList sync.Map + +// Define AcceptList here as maps using 'AcceptList' +// e.x) AcceptList.Store("/cosmos.auth.v1beta1.Query/Account", &authtypes.QueryAccountResponse{}) +func init() { + +}