From 8689e061d993aa6f5f2829afff847f2d8d4cdf67 Mon Sep 17 00:00:00 2001 From: srdtrk <59252793+srdtrk@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:04:50 +0300 Subject: [PATCH] feat(08-wasm): querier plugins implemented (#5345) * imp: moved gas to internal * fix: fixed compiler complaints * feat: added 'QueryRouter' interface * imp: passing the queryRouter to keeper * Revert "fix: fixed compiler complaints" This reverts commit 208e3149115b4c2e1577bed8cbb230db4a7f42fa. * Revert "imp: moved gas to internal" This reverts commit b45b60570099d1db3b946fe970742a88fa1050bc. * fix(test): fixed keeper_test.go * imp: initial querier template * imp: moved querier to types * fix: compiler complaints * imp: removed querier from keeper * feat: including default querier * imp: added options * feat: querier implemented fully * docs: improved godocs * imp: improved the querier * style: improved styling of querier * fix: fixed options * fix: fixed options not being passed with config * style: renamed variables * imp: added review items * imp: review items * imp: set and get query plugins made private * docs: added more godocs * fix: default plugin not set * imp: review items * docs: added a godoc --------- Co-authored-by: Carlos Rodriguez (cherry picked from commit e2bcb775c75d38ffdbc7c8960cc463d04d77b760) --- .../internal/ibcwasm/expected_interfaces.go | 14 ++ .../08-wasm/internal/ibcwasm/querier.go | 17 -- .../08-wasm/internal/ibcwasm/wasm.go | 37 ++-- .../light-clients/08-wasm/keeper/keeper.go | 34 ++-- .../08-wasm/keeper/keeper_test.go | 25 ++- .../08-wasm/keeper/migrations.go | 2 +- .../light-clients/08-wasm/keeper/options.go | 32 ++++ .../08-wasm/keeper/options_test.go | 154 +++++++++++++++ .../08-wasm/testing/simapp/app.go | 4 +- modules/light-clients/08-wasm/types/events.go | 13 ++ .../light-clients/08-wasm/types/querier.go | 178 ++++++++++++++++++ .../08-wasm/types/querier_test.go | 134 ++++++++++--- modules/light-clients/08-wasm/types/vm.go | 8 +- 13 files changed, 574 insertions(+), 78 deletions(-) delete mode 100644 modules/light-clients/08-wasm/internal/ibcwasm/querier.go create mode 100644 modules/light-clients/08-wasm/keeper/options.go create mode 100644 modules/light-clients/08-wasm/keeper/options_test.go create mode 100644 modules/light-clients/08-wasm/types/querier.go diff --git a/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go b/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go index 5b57a2f0d0b..964b3b1469e 100644 --- a/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go +++ b/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go @@ -3,6 +3,9 @@ package ibcwasm import ( wasmvm "github.com/CosmWasm/wasmvm" wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" ) var _ WasmEngine = (*wasmvm.VM)(nil) @@ -115,3 +118,14 @@ type WasmEngine interface { // Unpin is idempotent. Unpin(checksum wasmvm.Checksum) error } + +type QueryRouter interface { + // Route returns the GRPCQueryHandler for a given query route path or nil + // if not found + Route(path string) baseapp.GRPCQueryHandler +} + +type QueryPluginsI interface { + // HandleQuery will route the query to the correct plugin and return the result + HandleQuery(ctx sdk.Context, caller string, request wasmvmtypes.QueryRequest) ([]byte, error) +} diff --git a/modules/light-clients/08-wasm/internal/ibcwasm/querier.go b/modules/light-clients/08-wasm/internal/ibcwasm/querier.go deleted file mode 100644 index a955d4f7c10..00000000000 --- a/modules/light-clients/08-wasm/internal/ibcwasm/querier.go +++ /dev/null @@ -1,17 +0,0 @@ -package ibcwasm - -import ( - "errors" - - wasmvmtypes "github.com/CosmWasm/wasmvm/types" -) - -type defaultQuerier struct{} - -func (*defaultQuerier) GasConsumed() uint64 { - return 0 -} - -func (*defaultQuerier) Query(_ wasmvmtypes.QueryRequest, _ uint64) ([]byte, error) { - return nil, errors.New("queries in contract are not allowed") -} diff --git a/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go b/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go index 6dbfc0c1c5f..ffa29c5b71e 100644 --- a/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go +++ b/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go @@ -3,8 +3,6 @@ package ibcwasm import ( "errors" - wasmvm "github.com/CosmWasm/wasmvm" - "cosmossdk.io/collections" storetypes "cosmossdk.io/core/store" ) @@ -12,7 +10,8 @@ import ( var ( vm WasmEngine - querier wasmvm.Querier + queryRouter QueryRouter + queryPlugins QueryPluginsI // state management Schema collections.Schema @@ -36,19 +35,31 @@ func GetVM() WasmEngine { return vm } -// SetQuerier sets the custom wasm query handle for the 08-wasm module. -// If wasmQuerier is nil a default querier is used that return always an error for any query. -func SetQuerier(wasmQuerier wasmvm.Querier) { - if wasmQuerier == nil { - querier = &defaultQuerier{} - } else { - querier = wasmQuerier +// SetQueryRouter sets the custom wasm query router for the 08-wasm module. +// Panics if the queryRouter is nil. +func SetQueryRouter(router QueryRouter) { + if router == nil { + panic(errors.New("query router must be not nil")) + } + queryRouter = router +} + +// GetQueryRouter returns the custom wasm query router for the 08-wasm module. +func GetQueryRouter() QueryRouter { + return queryRouter +} + +// SetQueryPlugins sets the current query plugins +func SetQueryPlugins(plugins QueryPluginsI) { + if plugins == nil { + panic(errors.New("query plugins must be not nil")) } + queryPlugins = plugins } -// GetQuerier returns the custom wasm query handler for the 08-wasm module. -func GetQuerier() wasmvm.Querier { - return querier +// GetQueryPlugins returns the current query plugins +func GetQueryPlugins() QueryPluginsI { + return queryPlugins } // SetupWasmStoreService sets up the 08-wasm module's collections. diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go index 96193edc874..8676601a230 100644 --- a/modules/light-clients/08-wasm/keeper/keeper.go +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -11,7 +11,6 @@ import ( storetypes "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" - "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -19,7 +18,6 @@ import ( "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - "github.com/cosmos/ibc-go/v8/modules/core/exported" ) // Keeper defines the 08-wasm keeper @@ -44,7 +42,8 @@ func NewKeeperWithVM( clientKeeper types.ClientKeeper, authority string, vm ibcwasm.WasmEngine, - querier wasmvm.Querier, + queryRouter ibcwasm.QueryRouter, + opts ...Option, ) Keeper { if clientKeeper == nil { panic(errors.New("client keeper must be not nil")) @@ -62,16 +61,25 @@ func NewKeeperWithVM( panic(errors.New("authority must be non-empty")) } - ibcwasm.SetVM(vm) - ibcwasm.SetQuerier(querier) - ibcwasm.SetupWasmStoreService(storeService) - - return Keeper{ + keeper := &Keeper{ cdc: cdc, storeService: storeService, clientKeeper: clientKeeper, authority: authority, } + + // set query plugins to ensure there is a non-nil query plugin + // regardless of what options the user provides + ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + for _, opt := range opts { + opt.apply(keeper) + } + + ibcwasm.SetVM(vm) + ibcwasm.SetQueryRouter(queryRouter) + ibcwasm.SetupWasmStoreService(storeService) + + return *keeper } // NewKeeperWithConfig creates a new Keeper instance with the provided Wasm configuration. @@ -83,14 +91,15 @@ func NewKeeperWithConfig( clientKeeper types.ClientKeeper, authority string, wasmConfig types.WasmConfig, - querier wasmvm.Querier, + queryRouter ibcwasm.QueryRouter, + opts ...Option, ) Keeper { vm, err := wasmvm.NewVM(wasmConfig.DataDir, wasmConfig.SupportedCapabilities, types.ContractMemoryLimit, wasmConfig.ContractDebugMode, types.MemoryCacheSize) if err != nil { panic(fmt.Errorf("failed to instantiate new Wasm VM instance: %v", err)) } - return NewKeeperWithVM(cdc, storeService, clientKeeper, authority, vm, querier) + return NewKeeperWithVM(cdc, storeService, clientKeeper, authority, vm, queryRouter, opts...) } // GetAuthority returns the 08-wasm module's authority. @@ -98,11 +107,6 @@ func (k Keeper) GetAuthority() string { return k.authority } -// Logger returns a module-specific logger. -func (Keeper) Logger(ctx sdk.Context) log.Logger { - return ctx.Logger().With("module", "x/"+exported.ModuleName+"-"+types.ModuleName) -} - func (Keeper) storeWasmCode(ctx sdk.Context, code []byte, storeFn func(code wasmvm.WasmCode) (wasmvm.Checksum, error)) ([]byte, error) { var err error if types.IsGzip(code) { diff --git a/modules/light-clients/08-wasm/keeper/keeper_test.go b/modules/light-clients/08-wasm/keeper/keeper_test.go index 00b915b373a..d6d26173419 100644 --- a/modules/light-clients/08-wasm/keeper/keeper_test.go +++ b/modules/light-clients/08-wasm/keeper/keeper_test.go @@ -158,7 +158,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), ibcwasm.GetVM(), - nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, true, @@ -173,7 +173,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, "", // authority ibcwasm.GetVM(), - nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, false, @@ -188,7 +188,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { nil, // client keeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), ibcwasm.GetVM(), - nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, false, @@ -203,7 +203,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), nil, - nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, false, @@ -218,12 +218,27 @@ func (suite *KeeperTestSuite) TestNewKeeper() { GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), ibcwasm.GetVM(), - nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, false, errors.New("store service must be not nil"), }, + { + "failure: nil query router", + func() { + keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + nil, + ) + }, + false, + errors.New("query router must be not nil"), + }, } for _, tc := range testCases { diff --git a/modules/light-clients/08-wasm/keeper/migrations.go b/modules/light-clients/08-wasm/keeper/migrations.go index 10bd5435e83..9961a7251a0 100644 --- a/modules/light-clients/08-wasm/keeper/migrations.go +++ b/modules/light-clients/08-wasm/keeper/migrations.go @@ -42,7 +42,7 @@ func (m Migrator) MigrateChecksums(ctx sdk.Context) error { return err } - m.keeper.Logger(ctx).Info("successfully migrated Checksums to collections") + types.Logger(ctx).Info("successfully migrated Checksums to collections") return nil } diff --git a/modules/light-clients/08-wasm/keeper/options.go b/modules/light-clients/08-wasm/keeper/options.go new file mode 100644 index 00000000000..8977a6df371 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/options.go @@ -0,0 +1,32 @@ +package keeper + +import ( + "errors" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +// Option is an extension point to instantiate keeper with non default values +type Option interface { + apply(*Keeper) +} + +type optsFn func(*Keeper) + +func (f optsFn) apply(keeper *Keeper) { + f(keeper) +} + +// WithQueryPlugins is an optional constructor parameter to pass custom query plugins for wasmVM requests. +// Missing fields will be filled with default queriers. +func WithQueryPlugins(plugins *types.QueryPlugins) Option { + return optsFn(func(_ *Keeper) { + currentPlugins, ok := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + if !ok { + panic(errors.New("invalid query plugins type")) + } + newPlugins := currentPlugins.Merge(plugins) + ibcwasm.SetQueryPlugins(&newPlugins) + }) +} diff --git a/modules/light-clients/08-wasm/keeper/options_test.go b/modules/light-clients/08-wasm/keeper/options_test.go new file mode 100644 index 00000000000..3f4680491ca --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/options_test.go @@ -0,0 +1,154 @@ +package keeper_test + +import ( + "encoding/json" + "errors" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +func MockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { + return func(_ sdk.Context, _ json.RawMessage) ([]byte, error) { + return nil, errors.New("custom querier error for TestNewKeeperWithOptions") + } +} + +func MockStargateQuerier() func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { + return func(_ sdk.Context, _ *wasmvmtypes.StargateQuery) ([]byte, error) { + return nil, errors.New("stargate querier error for TestNewKeeperWithOptions") + } +} + +func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { + var k keeper.Keeper + testCases := []struct { + name string + malleate func() + verifyFn func(keeper.Keeper) + }{ + { + "success: no options", + func() { + k = keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + ) + }, + func(k keeper.Keeper) { + plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + + _, err := plugins.Custom(sdk.Context{}, nil) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) + + _, err = plugins.Stargate(sdk.Context{}, &wasmvmtypes.StargateQuery{}) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "'' path is not allowed from the contract"}) + }, + }, + { + "success: custom querier", + func() { + querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ + Custom: MockCustomQuerier(), + }) + k = keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + querierOption, + ) + }, + func(k keeper.Keeper) { + plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + + _, err := plugins.Custom(sdk.Context{}, nil) + suite.Require().ErrorContains(err, "custom querier error for TestNewKeeperWithOptions") + + _, err = plugins.Stargate(sdk.Context{}, &wasmvmtypes.StargateQuery{}) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "'' path is not allowed from the contract"}) + }, + }, + { + "success: stargate querier", + func() { + querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ + Stargate: MockStargateQuerier(), + }) + k = keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + querierOption, + ) + }, + func(k keeper.Keeper) { + plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + + _, err := plugins.Custom(sdk.Context{}, nil) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) + + _, err = plugins.Stargate(sdk.Context{}, &wasmvmtypes.StargateQuery{}) + suite.Require().ErrorContains(err, "stargate querier error for TestNewKeeperWithOptions") + }, + }, + { + "success: both queriers", + func() { + querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ + Custom: MockCustomQuerier(), + Stargate: MockStargateQuerier(), + }) + k = keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + querierOption, + ) + }, + func(k keeper.Keeper) { + plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + + _, err := plugins.Custom(sdk.Context{}, nil) + suite.Require().ErrorContains(err, "custom querier error for TestNewKeeperWithOptions") + + _, err = plugins.Stargate(sdk.Context{}, &wasmvmtypes.StargateQuery{}) + suite.Require().ErrorContains(err, "stargate querier error for TestNewKeeperWithOptions") + }, + }, + } + + for _, tc := range testCases { + tc := tc + suite.SetupTest() + + suite.Run(tc.name, func() { + // make sure the default query plugins are set + ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + + tc.malleate() + tc.verifyFn(k) + + // reset query plugins after each test + ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + }) + } +} diff --git a/modules/light-clients/08-wasm/testing/simapp/app.go b/modules/light-clients/08-wasm/testing/simapp/app.go index d3794fb8896..766bc5d0234 100644 --- a/modules/light-clients/08-wasm/testing/simapp/app.go +++ b/modules/light-clients/08-wasm/testing/simapp/app.go @@ -471,12 +471,12 @@ func NewSimApp( // NOTE: mockVM is used for testing purposes only! app.WasmClientKeeper = wasmkeeper.NewKeeperWithVM( appCodec, runtime.NewKVStoreService(keys[wasmtypes.StoreKey]), app.IBCKeeper.ClientKeeper, - authtypes.NewModuleAddress(govtypes.ModuleName).String(), mockVM, nil, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), mockVM, app.GRPCQueryRouter(), ) } else { app.WasmClientKeeper = wasmkeeper.NewKeeperWithConfig( appCodec, runtime.NewKVStoreService(keys[wasmtypes.StoreKey]), app.IBCKeeper.ClientKeeper, - authtypes.NewModuleAddress(govtypes.ModuleName).String(), wasmConfig, nil, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), wasmConfig, app.GRPCQueryRouter(), ) } diff --git a/modules/light-clients/08-wasm/types/events.go b/modules/light-clients/08-wasm/types/events.go index c8290fe8579..d2d76f706a1 100644 --- a/modules/light-clients/08-wasm/types/events.go +++ b/modules/light-clients/08-wasm/types/events.go @@ -1,5 +1,13 @@ package types +import ( + "cosmossdk.io/log" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + // IBC 08-wasm events const ( // EventTypeStoreWasmCode defines the event type for bytecode storage @@ -16,3 +24,8 @@ const ( AttributeValueCategory = ModuleName ) + +// Logger returns a module-specific logger. +func Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", "x/"+exported.ModuleName+"-"+ModuleName) +} diff --git a/modules/light-clients/08-wasm/types/querier.go b/modules/light-clients/08-wasm/types/querier.go new file mode 100644 index 00000000000..e37b9bc9ad3 --- /dev/null +++ b/modules/light-clients/08-wasm/types/querier.go @@ -0,0 +1,178 @@ +package types + +import ( + "encoding/json" + "fmt" + "slices" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" +) + +/* +`queryHandler` is a contextual querier which references the global `ibcwasm.QueryPluginsI` +to handle queries. The global `ibcwasm.QueryPluginsI` points to a `types.QueryPlugins` which +contains two sub-queriers: `types.CustomQuerier` and `types.StargateQuerier`. These sub-queriers +can be replaced by the user through the options api in the keeper. + +In addition, the `types.StargateQuerier` references a global `ibcwasm.QueryRouter` which points +to `baseapp.GRPCQueryRouter`. + +This design is based on wasmd's (v0.50.0) querier plugin design. +*/ + +var ( + _ wasmvmtypes.Querier = (*queryHandler)(nil) + _ ibcwasm.QueryPluginsI = (*QueryPlugins)(nil) +) + +// queryHandler is a wrapper around the sdk.Context and the CallerID that calls +// into the query plugins. +type queryHandler struct { + Ctx sdk.Context + CallerID string +} + +// newQueryHandler returns a default querier that can be used in the contract. +func newQueryHandler(ctx sdk.Context, callerID string) *queryHandler { + return &queryHandler{ + Ctx: ctx, + CallerID: callerID, + } +} + +// GasConsumed implements the wasmvmtypes.Querier interface. +func (q *queryHandler) GasConsumed() uint64 { + return VMGasRegister.ToWasmVMGas(q.Ctx.GasMeter().GasConsumed()) +} + +// Query implements the wasmvmtypes.Querier interface. +func (q *queryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) ([]byte, error) { + sdkGas := VMGasRegister.FromWasmVMGas(gasLimit) + + // discard all changes/events in subCtx by not committing the cached context + subCtx, _ := q.Ctx.WithGasMeter(storetypes.NewGasMeter(sdkGas)).CacheContext() + + // make sure we charge the higher level context even on panic + defer func() { + q.Ctx.GasMeter().ConsumeGas(subCtx.GasMeter().GasConsumed(), "contract sub-query") + }() + + res, err := ibcwasm.GetQueryPlugins().HandleQuery(subCtx, q.CallerID, request) + if err == nil { + return res, nil + } + + Logger(q.Ctx).Debug("Redacting query error", "cause", err) + return nil, redactError(err) +} + +type ( + CustomQuerier func(ctx sdk.Context, request json.RawMessage) ([]byte, error) + StargateQuerier func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) + + // QueryPlugins is a list of queriers that can be used to extend the default querier. + QueryPlugins struct { + Custom CustomQuerier + Stargate StargateQuerier + } +) + +// Merge merges the query plugin with a provided one. +func (e QueryPlugins) Merge(x *QueryPlugins) QueryPlugins { + // only update if this is non-nil and then only set values + if x == nil { + return e + } + + if x.Custom != nil { + e.Custom = x.Custom + } + + if x.Stargate != nil { + e.Stargate = x.Stargate + } + + return e +} + +// HandleQuery implements the ibcwasm.QueryPluginsI interface. +func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller string, request wasmvmtypes.QueryRequest) ([]byte, error) { + if request.Stargate != nil { + return e.Stargate(ctx, request.Stargate) + } + + if request.Custom != nil { + return e.Custom(ctx, request.Custom) + } + + return nil, wasmvmtypes.UnsupportedRequest{Kind: "Unsupported query request"} +} + +// NewDefaultQueryPlugins returns the default set of query plugins +func NewDefaultQueryPlugins() *QueryPlugins { + return &QueryPlugins{ + Custom: RejectCustomQuerier(), + Stargate: AcceptListStargateQuerier([]string{}), + } +} + +// AcceptListStargateQuerier allows all queries that are in the provided accept list. +// This function returns protobuf encoded responses in bytes. +func AcceptListStargateQuerier(acceptedQueries []string) func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + // A default list of accepted queries can be added here. + // accepted = append(defaultAcceptList, accepted...) + + isAccepted := slices.Contains(acceptedQueries, request.Path) + if !isAccepted { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)} + } + + route := ibcwasm.GetQueryRouter().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 + } + if res == nil || res.Value == nil { + return nil, wasmvmtypes.InvalidResponse{Err: "Query response is empty"} + } + + return res.Value, nil + } +} + +// RejectCustomQuerier rejects all custom queries +func RejectCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + return nil, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"} + } +} + +// Wasmd Issue [#759](https://github.com/CosmWasm/wasmd/issues/759) +// Don't return error string for worries of non-determinism +func redactError(err error) error { + // Do not redact system errors + // SystemErrors must be created in 08-wasm and we can ensure determinism + if wasmvmtypes.ToSystemError(err) != nil { + return err + } + + codespace, code, _ := errorsmod.ABCIInfo(err, false) + return fmt.Errorf("codespace: %s, code: %d", codespace, code) +} diff --git a/modules/light-clients/08-wasm/types/querier_test.go b/modules/light-clients/08-wasm/types/querier_test.go index f3a6ce7745a..89a9cf911f6 100644 --- a/modules/light-clients/08-wasm/types/querier_test.go +++ b/modules/light-clients/08-wasm/types/querier_test.go @@ -1,12 +1,16 @@ package types_test import ( + "encoding/hex" "encoding/json" + "fmt" "math" wasmvm "github.com/CosmWasm/wasmvm" wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" @@ -21,25 +25,21 @@ type QueryEcho struct { Data string `json:"data"` } -type CustomQueryHandler struct{} +func MockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + var customQuery CustomQuery + err := json.Unmarshal([]byte(request), &customQuery) + if err != nil { + return nil, wasmtesting.ErrMockContract + } -func (*CustomQueryHandler) GasConsumed() uint64 { - return 0 -} + if customQuery.Echo != nil { + data, err := json.Marshal(customQuery.Echo.Data) + return data, err + } -func (*CustomQueryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) ([]byte, error) { - var customQuery CustomQuery - err := json.Unmarshal([]byte(request.Custom), &customQuery) - if err != nil { return nil, wasmtesting.ErrMockContract } - - if customQuery.Echo != nil { - data, err := json.Marshal(customQuery.Echo.Data) - return data, err - } - - return nil, wasmtesting.ErrMockContract } func (suite *TypesTestSuite) TestCustomQuery() { @@ -50,9 +50,12 @@ func (suite *TypesTestSuite) TestCustomQuery() { { "success: custom query", func() { - ibcwasm.SetQuerier(&CustomQueryHandler{}) + querierPlugin := types.QueryPlugins{ + Custom: MockCustomQuerier(), + } + ibcwasm.SetQueryPlugins(&querierPlugin) - suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(checksum wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) { + suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { echo := CustomQuery{ Echo: &QueryEcho{ Data: "hello world", @@ -79,11 +82,12 @@ func (suite *TypesTestSuite) TestCustomQuery() { }, }, { - "default query", + "failure: default query", func() { - suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(checksum wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) { - _, err := querier.Query(wasmvmtypes.QueryRequest{}, math.MaxUint64) - suite.Require().Error(err) + suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { + resp, err := querier.Query(wasmvmtypes.QueryRequest{Custom: json.RawMessage("{}")}, math.MaxUint64) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) + suite.Require().Nil(resp) return nil, wasmtesting.DefaultGasUsed, err }) @@ -104,6 +108,94 @@ func (suite *TypesTestSuite) TestCustomQuery() { clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) clientState := endpoint.GetClientState() clientState.Status(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec()) + + // reset query plugins after each test + ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + }) + } +} + +func (suite *TypesTestSuite) TestStargateQuery() { + typeURL := "/ibc.lightclients.wasm.v1.Query/Checksums" + + testCases := []struct { + name string + malleate func() + }{ + { + "success: custom query", + func() { + querierPlugin := types.QueryPlugins{ + Stargate: types.AcceptListStargateQuerier([]string{typeURL}), + } + + ibcwasm.SetQueryPlugins(&querierPlugin) + + suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { + queryRequest := types.QueryChecksumsRequest{} + bz, err := queryRequest.Marshal() + suite.Require().NoError(err) + + resp, err := querier.Query(wasmvmtypes.QueryRequest{ + Stargate: &wasmvmtypes.StargateQuery{ + Path: typeURL, + Data: bz, + }, + }, math.MaxUint64) + suite.Require().NoError(err) + + var respData types.QueryChecksumsResponse + err = respData.Unmarshal(resp) + suite.Require().NoError(err) + + expChecksum := hex.EncodeToString(suite.checksum) + + suite.Require().Len(respData.Checksums, 1) + suite.Require().Equal(expChecksum, respData.Checksums[0]) + + return resp, wasmtesting.DefaultGasUsed, nil + }) + }, + }, + { + "failure: default querier", + func() { + suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { + queryRequest := types.QueryChecksumsRequest{} + bz, err := queryRequest.Marshal() + suite.Require().NoError(err) + + resp, err := querier.Query(wasmvmtypes.QueryRequest{ + Stargate: &wasmvmtypes.StargateQuery{ + Path: typeURL, + Data: bz, + }, + }, math.MaxUint64) + suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", typeURL)}) + suite.Require().Nil(resp) + + return nil, wasmtesting.DefaultGasUsed, err + }) + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + tc.malleate() + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState := endpoint.GetClientState() + clientState.Status(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec()) + + // reset query plugins after each test + ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) }) } } diff --git a/modules/light-clients/08-wasm/types/vm.go b/modules/light-clients/08-wasm/types/vm.go index ab44e2466b9..1a894ae0843 100644 --- a/modules/light-clients/08-wasm/types/vm.go +++ b/modules/light-clients/08-wasm/types/vm.go @@ -49,7 +49,7 @@ func instantiateContract(ctx sdk.Context, clientStore storetypes.KVStore, checks } ctx.GasMeter().ConsumeGas(VMGasRegister.NewContractInstanceCosts(true, len(msg)), "Loading CosmWasm module: instantiate") - response, gasUsed, err := ibcwasm.GetVM().Instantiate(checksum, env, msgInfo, msg, newStoreAdapter(clientStore), wasmvmAPI, ibcwasm.GetQuerier(), multipliedGasMeter, gasLimit, costJSONDeserialization) + response, gasUsed, err := ibcwasm.GetVM().Instantiate(checksum, env, msgInfo, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) VMGasRegister.consumeRuntimeGas(ctx, gasUsed) return response, err } @@ -67,7 +67,7 @@ func callContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Chec env := getEnv(ctx, clientID) ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: sudo") - resp, gasUsed, err := ibcwasm.GetVM().Sudo(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, ibcwasm.GetQuerier(), multipliedGasMeter, gasLimit, costJSONDeserialization) + resp, gasUsed, err := ibcwasm.GetVM().Sudo(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) VMGasRegister.consumeRuntimeGas(ctx, gasUsed) return resp, err } @@ -81,7 +81,7 @@ func migrateContract(ctx sdk.Context, clientID string, clientStore storetypes.KV env := getEnv(ctx, clientID) ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: migrate") - resp, gasUsed, err := ibcwasm.GetVM().Migrate(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, ibcwasm.GetQuerier(), multipliedGasMeter, gasLimit, costJSONDeserialization) + resp, gasUsed, err := ibcwasm.GetVM().Migrate(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) VMGasRegister.consumeRuntimeGas(ctx, gasUsed) return resp, err } @@ -99,7 +99,7 @@ func queryContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Che env := getEnv(ctx, clientID) ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: query") - resp, gasUsed, err := ibcwasm.GetVM().Query(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, ibcwasm.GetQuerier(), multipliedGasMeter, gasLimit, costJSONDeserialization) + resp, gasUsed, err := ibcwasm.GetVM().Query(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) VMGasRegister.consumeRuntimeGas(ctx, gasUsed) return resp, err }