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

Enable whitelisted Stargate Queries #971

Closed
Closed
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
2 changes: 1 addition & 1 deletion x/wasm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
59 changes: 55 additions & 4 deletions x/wasm/keeper/query_plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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)}
mattverse marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
}

Expand Down Expand Up @@ -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.
Expand Down
161 changes: 150 additions & 11 deletions x/wasm/keeper/query_plugins_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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
Expand All @@ -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",
},
})
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
mattverse marked this conversation as resolved.
Show resolved Hide resolved

// raw build also make same result
jsonMarshalExpectedResponse, err := wasmApp.AppCodec().MarshalJSON(tc.expectedProto())
require.NoError(t, err)
require.Equal(t, jsonMarshalledUpdatedBz, jsonMarshalExpectedResponse)
})
}
}
8 changes: 4 additions & 4 deletions x/wasm/keeper/reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions x/wasm/keeper/stargate_whitelist.go
Original file line number Diff line number Diff line change
@@ -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() {

}