From f44b60c92837b52d9f437e68cc941d25621485c0 Mon Sep 17 00:00:00 2001 From: son trinh Date: Mon, 15 Jan 2024 20:47:22 +0700 Subject: [PATCH] feat(statemachine)!: Add allow all client wildcard to AllowedClients param (#5429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add wildcard allow all client * minor * fix lint * minor and doc update * review comments * if allow all clients is present, no other client types should be in the allow list * clean up code to allow wasm client type * remove unused variable * Fix build failures, tweak err message. * modify allow clients list in genesis with feature releases * chore: adding v8.1 to minor versions in e2e feat releases struct * update docs * chore: doc lint fixes --------- Co-authored-by: GnaD <89174180+GNaD13@users.noreply.github.com> Co-authored-by: Du Nguyen <61083705+lichdu29@users.noreply.github.com> Co-authored-by: Carlos Rodriguez Co-authored-by: DimitrisJim Co-authored-by: Damian Nolan Co-authored-by: Đỗ Việt Hoàng Co-authored-by: Charly (cherry picked from commit d5949b1a722eafc2eff6508fbdf757425949611a) # Conflicts: # docs/docs/03-light-clients/04-wasm/03-integration.md # e2e/tests/wasm/grandpa_test.go # e2e/testsuite/testconfig.go # e2e/testvalues/values.go # modules/core/02-client/types/params.go # modules/core/02-client/types/params_test.go # modules/light-clients/08-wasm/keeper/keeper_test.go # modules/light-clients/08-wasm/testing/wasm_endpoint.go # modules/light-clients/08-wasm/types/types_test.go --- docs/docs/01-ibc/11-troubleshooting.md | 2 +- .../01-developer-guide/09-setup.md | 2 + .../02-localhost/02-integration.md | 9 +- .../04-wasm/03-integration.md | 368 +++++++++ docs/params/params.md | 17 +- e2e/tests/wasm/grandpa_test.go | 768 ++++++++++++++++++ e2e/testsuite/testconfig.go | 725 +++++++++++++++++ e2e/testvalues/values.go | 101 +++ modules/core/02-client/types/keys.go | 4 + modules/core/02-client/types/params.go | 27 +- modules/core/02-client/types/params_test.go | 7 + .../08-wasm/keeper/keeper_test.go | 326 ++++++++ .../08-wasm/testing/wasm_endpoint.go | 52 ++ .../light-clients/08-wasm/types/types_test.go | 131 +++ 14 files changed, 2523 insertions(+), 16 deletions(-) create mode 100644 docs/docs/03-light-clients/04-wasm/03-integration.md create mode 100644 e2e/tests/wasm/grandpa_test.go create mode 100644 e2e/testsuite/testconfig.go create mode 100644 e2e/testvalues/values.go create mode 100644 modules/light-clients/08-wasm/keeper/keeper_test.go create mode 100644 modules/light-clients/08-wasm/testing/wasm_endpoint.go create mode 100644 modules/light-clients/08-wasm/types/types_test.go diff --git a/docs/docs/01-ibc/11-troubleshooting.md b/docs/docs/01-ibc/11-troubleshooting.md index ba9dde39256..4080f3b449b 100644 --- a/docs/docs/01-ibc/11-troubleshooting.md +++ b/docs/docs/01-ibc/11-troubleshooting.md @@ -12,4 +12,4 @@ slug: /ibc/troubleshooting If it is being reported that a client state is unauthorized, this is due to the client type not being present in the [`AllowedClients`](https://github.com/cosmos/ibc-go/blob/v6.0.0/modules/core/02-client/types/client.pb.go#L345) array. -Unless the client type is present in this array, all usage of clients of this type will be prevented. +Unless the client type is present in this array or the `AllowAllClients` wildcard (`"*"`) is used, all usage of clients of this type will be prevented. diff --git a/docs/docs/03-light-clients/01-developer-guide/09-setup.md b/docs/docs/03-light-clients/01-developer-guide/09-setup.md index f3bba760ec2..7ccbf4a5833 100644 --- a/docs/docs/03-light-clients/01-developer-guide/09-setup.md +++ b/docs/docs/03-light-clients/01-developer-guide/09-setup.md @@ -131,3 +131,5 @@ where `proposal.json` contains: "deposit": "100stake" } ``` + +If the `AllowedClients` list contains a single element that is equal to the wildcard `"*"`, then all client types are allowed and it is thus not necessary to submit a governance proposal to update the parameter. diff --git a/docs/docs/03-light-clients/02-localhost/02-integration.md b/docs/docs/03-light-clients/02-localhost/02-integration.md index 01f77fce67e..e1fc491a332 100644 --- a/docs/docs/03-light-clients/02-localhost/02-integration.md +++ b/docs/docs/03-light-clients/02-localhost/02-integration.md @@ -10,11 +10,10 @@ slug: /ibc/light-clients/localhost/integration The 09-localhost light client module registers codec types within the core IBC module. This differs from other light client module implementations which are expected to register codec types using the `AppModuleBasic` interface. -The localhost client is added to the 02-client submodule param [`allowed_clients`](https://github.com/cosmos/ibc-go/blob/v7.0.0/proto/ibc/core/client/v1/client.proto#L102) by default in ibc-go. +The localhost client is implicitly enabled by using the `AllowAllClients` wildcard (`"*"`) in the 02-client submodule default value for param [`allowed_clients`](https://github.com/cosmos/ibc-go/blob/v7.0.0/proto/ibc/core/client/v1/client.proto#L102). ```go -var ( - // DefaultAllowedClients are the default clients for the AllowedClients parameter. - DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint, exported.Localhost} -) +// DefaultAllowedClients are the default clients for the AllowedClients parameter. +// By default it allows all client types. +var DefaultAllowedClients = []string{AllowAllClients} ``` diff --git a/docs/docs/03-light-clients/04-wasm/03-integration.md b/docs/docs/03-light-clients/04-wasm/03-integration.md new file mode 100644 index 00000000000..3827203e6cd --- /dev/null +++ b/docs/docs/03-light-clients/04-wasm/03-integration.md @@ -0,0 +1,368 @@ +--- +title: Integration +sidebar_label: Integration +sidebar_position: 3 +slug: /ibc/light-clients/wasm/integration +--- + +# Integration + +Learn how to integrate the `08-wasm` module in a chain binary and about the recommended approaches depending on whether the [`x/wasm` module](https://github.com/CosmWasm/wasmd/tree/main/x/wasm) is already used in the chain. The following document only applies for Cosmos SDK chains. + +## `app.go` setup + +The sample code below shows the relevant integration points in `app.go` required to setup the `08-wasm` module in a chain binary. Since `08-wasm` is a light client module itself, please check out as well the section [Integrating light clients](../../01-ibc/02-integration.md#integrating-light-clients) for more information: + +```go +// app.go +import ( + ... + "github.com/cosmos/cosmos-sdk/runtime" + + cmtos "github.com/cometbft/cometbft/libs/os" + + ibcwasm "github.com/cosmos/ibc-go/modules/light-clients/08-wasm" + ibcwasmkeeper "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" + ibcwasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + ... +) + +... + +// Register the AppModule for the 08-wasm module +ModuleBasics = module.NewBasicManager( + ... + ibcwasm.AppModuleBasic{}, + ... +) + +// Add 08-wasm Keeper +type SimApp struct { + ... + WasmClientKeeper ibcwasmkeeper.Keeper + ... +} + +func NewSimApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + loadLatest bool, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), +) *SimApp { + ... + keys := sdk.NewKVStoreKeys( + ... + ibcwasmtypes.StoreKey, + ) + + // Instantiate 08-wasm's keeper + // This sample code uses a constructor function that + // accepts a pointer to an existing instance of Wasm VM. + // This is the recommended approach when the chain + // also uses `x/wasm`, and then the Wasm VM instance + // can be shared. + app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithVM( + appCodec, + runtime.NewKVStoreService(keys[ibcwasmtypes.StoreKey]), + app.IBCKeeper.ClientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmVM, + app.GRPCQueryRouter(), + ) + app.ModuleManager = module.NewManager( + // SDK app modules + ... + ibcwasm.NewAppModule(app.WasmClientKeeper), + ) + app.ModuleManager.SetOrderBeginBlockers( + ... + ibcwasmtypes.ModuleName, + ... + ) + app.ModuleManager.SetOrderEndBlockers( + ... + ibcwasmtypes.ModuleName, + ... + ) + genesisModuleOrder := []string{ + ... + ibcwasmtypes.ModuleName, + ... + } + app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...) + app.ModuleManager.SetOrderExportGenesis(genesisModuleOrder...) + ... + + // initialize BaseApp + app.SetInitChainer(app.InitChainer) + ... + + // must be before Loading version + if manager := app.SnapshotManager(); manager != nil { + err := manager.RegisterExtensions( + ibcwasmkeeper.NewWasmSnapshotter(app.CommitMultiStore(), &app.WasmClientKeeper), + ) + if err != nil { + panic(fmt.Errorf("failed to register snapshot extension: %s", err)) + } + } + ... + + if loadLatest { + ... + + ctx := app.BaseApp.NewUncachedContext(true, cmtproto.Header{}) + + // Initialize pinned codes in wasmvm as they are not persisted there + if err := ibcwasmkeeper.InitializePinnedCodes(ctx); err != nil { + cmtos.Exit(fmt.Sprintf("failed initialize pinned codes %s", err)) + } + } +} +``` + +## Keeper instantiation + +When it comes to instantiating `08-wasm`'s keeper there are two recommended ways of doing it. Choosing one or the other will depend on whether the chain already integrates [`x/wasm`](https://github.com/CosmWasm/wasmd/tree/main/x/wasm) or not. + +### If `x/wasm` is present + +If the chain where the module is integrated uses `x/wasm` then we recommend that both `08-wasm` and `x/wasm` share the same Wasm VM instance. Having two separate Wasm VM instances is still possible, but care should be taken to make sure that both instances do not share the directory when the VM stores blobs and various caches, otherwise unexpected behaviour is likely to happen. + +In order to share the Wasm VM instance please follow the guideline below. Please note that this requires `x/wasm`v0.41 or above. + +- Instantiate the Wasm VM in `app.go` with the parameters of your choice. +- [Create an `Option` with this Wasm VM instance](https://github.com/CosmWasm/wasmd/blob/db93d7b6c7bb6f4a340d74b96a02cec885729b59/x/wasm/keeper/options.go#L21-L25). +- Add the option created in the previous step to a slice and [pass it to the `x/wasm NewKeeper` constructor function](https://github.com/CosmWasm/wasmd/blob/db93d7b6c7bb6f4a340d74b96a02cec885729b59/x/wasm/keeper/keeper_cgo.go#L36). +- Pass the pointer to the Wasm VM instance to `08-wasm` [NewKeeperWithVM constructor function](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/keeper/keeper.go#L39-L47). + +The code to set this up would look something like this: + +```go +// app.go +import ( + ... + "github.com/cosmos/cosmos-sdk/runtime" + + wasmvm "github.com/CosmWasm/wasmvm" + ibcwasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + ... +) + +... + +// instantiate the Wasm VM with the chosen parameters +wasmer, err := wasmvm.NewVM( + dataDir, + availableCapabilities, + contractMemoryLimit, // default of 32 + contractDebugMode, + memoryCacheSize, +) +if err != nil { + panic(err) +} + +// create an Option slice (or append to an existing one) +// with the option to use a custom Wasm VM instance +wasmOpts = []ibcwasmkeeper.Option{ + ibcwasmkeeper.WithWasmEngine(wasmer), +} + +// the keeper will use the provided Wasm VM instance, +// instead of instantiating a new one +app.WasmKeeper = ibcwasmkeeper.NewKeeper( + appCodec, + keys[ibcwasmtypes.StoreKey], + app.AccountKeeper, + app.BankKeeper, + app.StakingKeeper, + distrkeeper.NewQuerier(app.DistrKeeper), + app.IBCFeeKeeper, // ISC4 Wrapper: fee IBC middleware + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + scopedWasmKeeper, + app.TransferKeeper, + app.MsgServiceRouter(), + app.GRPCQueryRouter(), + wasmDir, + wasmConfig, + availableCapabilities, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmOpts..., +) + +app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithVM( + appCodec, + runtime.NewKVStoreService(keys[ibcwasmtypes.StoreKey]), + app.IBCKeeper.ClientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmer, // pass the Wasm VM instance to `08-wasm` keeper constructor + app.GRPCQueryRouter(), +) +... +``` + +### If `x/wasm` is not present + +If the chain does not use [`x/wasm`](https://github.com/CosmWasm/wasmd/tree/main/x/wasm), even though it is still possible to use the method above from the previous section +(e.g. instantiating a Wasm VM in app.go an pass it to 08-wasm's [`NewKeeperWithVM` constructor function](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/keeper/keeper.go#L39-L47), since there would be no need in this case to share the Wasm VM instance with another module, you can use the [`NewKeeperWithConfig`` constructor function](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/keeper/keeper.go#L88-L96) and provide the Wasm VM configuration parameters of your choice instead. A Wasm VM instance will be created in`NewKeeperWithConfig`. The parameters that can set are: + +- `DataDir` is the [directory for Wasm blobs and various caches](https://github.com/CosmWasm/wasmvm/blob/1638725b25d799f078d053391945399cb35664b1/lib.go#L25). In `wasmd` this is set to the [`wasm` folder under the home directory](https://github.com/CosmWasm/wasmd/blob/36416def20effe47fb77f29f5ba35a003970fdba/app/app.go#L578). +- `SupportedCapabilities` is a comma separated [list of capabilities supported by the chain](https://github.com/CosmWasm/wasmvm/blob/1638725b25d799f078d053391945399cb35664b1/lib.go#L26). [`wasmd` sets this to all the available capabilities](https://github.com/CosmWasm/wasmd/blob/36416def20effe47fb77f29f5ba35a003970fdba/app/app.go#L586), but 08-wasm only requires `iterator`. +- `MemoryCacheSize` sets [the size in MiB of an in-memory cache for e.g. module caching](https://github.com/CosmWasm/wasmvm/blob/1638725b25d799f078d053391945399cb35664b1/lib.go#L29C16-L29C104). It is not consensus-critical and should be defined on a per-node basis, often in the range 100 to 1000 MB. [`wasmd` reads this value of](https://github.com/CosmWasm/wasmd/blob/36416def20effe47fb77f29f5ba35a003970fdba/app/app.go#L579). Default value is 256. +- `ContractDebugMode` is a [flag to enable/disable printing debug logs from the contract to STDOUT](https://github.com/CosmWasm/wasmvm/blob/1638725b25d799f078d053391945399cb35664b1/lib.go#L28). This should be false in production environments. Default value is false. + +Another configuration parameter of the Wasm VM is the contract memory limit (in MiB), which is [set to 32](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/types/config.go#L8), [following the example of `wasmd`](https://github.com/CosmWasm/wasmd/blob/36416def20effe47fb77f29f5ba35a003970fdba/x/wasm/keeper/keeper.go#L32-L34). This parameter is not configurable by users of `08-wasm`. + +The following sample code shows how the keeper would be constructed using this method: + +```go +// app.go +import ( + ... + "github.com/cosmos/cosmos-sdk/runtime" + + ibcwasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + ibcwasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + ... +) +... +wasmConfig := ibcwasmtypes.WasmConfig{ + DataDir: "ibc_08-wasm_client_data", + SupportedCapabilities: "iterator", + ContractDebugMode: false, +} +app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithConfig( + appCodec, + runtime.NewKVStoreService(keys[ibcwasmtypes.StoreKey]), + app.IBCKeeper.ClientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmConfig, + app.GRPCQueryRouter(), +) +``` + +Check out also the [`WasmConfig` type definition](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/types/config.go#L21-L31) for more information on each of the configurable parameters. Some parameters allow node-level configurations. There is additionally the function [`DefaultWasmConfig`](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/types/config.go#L36-L42) available that returns a configuration with the default values. + +### Options + +The `08-wasm` module comes with an options API inspired by the one in `x/wasm`. +Currently the only option available is the `WithQueryPlugins` option, which allows registration of custom query plugins for the `08-wasm` module. The use of this API is optional and it is only required if the chain wants to register custom query plugins for the `08-wasm` module. + +#### `WithQueryPlugins` + +By default, the `08-wasm` module does not support any queries. However, it is possible to register custom query plugins for [`QueryRequest::Custom`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.0/packages/std/src/query/mod.rs#L45) and [`QueryRequest::Stargate`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.0/packages/std/src/query/mod.rs#L54-L61). + +Assuming that the keeper is not yet instantiated, the following sample code shows how to register query plugins for the `08-wasm` module. + +We first construct a [`QueryPlugins`](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/types/querier.go#L78-L87) object with the desired query plugins: + +```go +queryPlugins := ibcwasmtypes.QueryPlugins { + Custom: MyCustomQueryPlugin(), + // `myAcceptList` is a `[]string` containing the list of gRPC query paths that the chain wants to allow for the `08-wasm` module to query. + // These queries must be registered in the chain's gRPC query router, be deterministic, and track their gas usage. + // The `AcceptListStargateQuerier` function will return a query plugin that will only allow queries for the paths in the `myAcceptList`. + // The query responses are encoded in protobuf unlike the implementation in `x/wasm`. + Stargate: ibcwasmtypes.AcceptListStargateQuerier(myAcceptList), +} +``` + +You may leave any of the fields in the `QueryPlugins` object as `nil` if you do not want to register a query plugin for that query type. + +Then, we pass the `QueryPlugins` object to the `WithQueryPlugins` option: + +```go +querierOption := ibcwasmkeeper.WithQueryPlugins(&queryPlugins) +``` + +Finally, we pass the option to the `NewKeeperWithConfig` or `NewKeeperWithVM` constructor function during [Keeper instantiation](#keeper-instantiation): + +```diff +app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithConfig( + appCodec, + runtime.NewKVStoreService(keys[ibcwasmtypes.StoreKey]), + app.IBCKeeper.ClientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmConfig, + app.GRPCQueryRouter(), ++ querierOption, +) +``` + +```diff +app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithVM( + appCodec, + runtime.NewKVStoreService(keys[ibcwasmtypes.StoreKey]), + app.IBCKeeper.ClientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + wasmer, // pass the Wasm VM instance to `08-wasm` keeper constructor + app.GRPCQueryRouter(), ++ querierOption, +) +``` + +## Updating `AllowedClients` + +If the chain's 02-client submodule parameter `AllowedClients` contains the single wildcard `"*"` element, then it is not necessary to do anything in order to allow the creation of `08-wasm` clients. However, if the parameter contains a list of client types (e.g. `["06-solomachine", "07-tendermint"]`), then in order to use the `08-wasm` module chains must update the [`AllowedClients` parameter](https://github.com/cosmos/ibc-go/blob/v8.0.0/proto/ibc/core/client/v1/client.proto#L64) of core IBC. This can be configured directly in the application upgrade handler with the sample code below: + +```go +import ( + ... + ibcwasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + ... +) + +... + +func CreateWasmUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + clientKeeper clientkeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + // explicitly update the IBC 02-client params, adding the wasm client type + params := clientKeeper.GetParams(ctx) + params.AllowedClients = append(params.AllowedClients, ibcwasmtypes.Wasm) + clientKeeper.SetParams(ctx, params) + + return mm.RunMigrations(ctx, configurator, vm) + } +} +``` + +Or alternatively the parameter can be updated via a governance proposal (see at the bottom of section [`Creating clients`](../01-developer-guide/09-setup.md#creating-clients) for an example of how to do this). + +## Adding the module to the store + +As part of the upgrade migration you must also add the module to the upgrades store. + +```go +func (app SimApp) RegisterUpgradeHandlers() { + + ... + + if upgradeInfo.Name == UpgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + storeUpgrades := storetypes.StoreUpgrades{ + Added: []string{ + ibcwasmtypes.ModuleName, + }, + } + + // configure store loader that checks if version == upgradeHeight and applies store upgrades + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + } +} +``` + +## Adding snapshot support + +In order to use the `08-wasm` module chains are required to register the `WasmSnapshotter` extension in the snapshot manager. This snapshotter takes care of persisting the external state, in the form of contract code, of the Wasm VM instance to disk when the chain is snapshotted. [This code](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/testing/simapp/app.go#L775-L782) should be placed in `NewSimApp` function in `app.go`. + +## Pin byte codes at start + +Wasm byte codes should be pinned to the WasmVM cache on every application start, therefore [this code](https://github.com/cosmos/ibc-go/blob/57fcdb9a9a9db9b206f7df2f955866dc4e10fef4/modules/light-clients/08-wasm/testing/simapp/app.go#L825-L830) should be placed in `NewSimApp` function in `app.go`. diff --git a/docs/params/params.md b/docs/params/params.md index f588e09f413..d67191606d1 100644 --- a/docs/params/params.md +++ b/docs/params/params.md @@ -8,14 +8,17 @@ slug: /params.md The 02-client submodule contains the following parameters: -| Key | Type | Default Value | -| ---------------- | -------- | ------------------------------------------------- | -| `AllowedClients` | []string | `"06-solomachine","07-tendermint","09-localhost"` | +| Key | Type | Default Value | +| ---------------- | -------- | ------------- | +| `AllowedClients` | []string | `"*"` | ### AllowedClients -The allowed clients parameter defines an allowlist of client types supported by the chain. A client -that is not registered on this list will fail upon creation or on genesis validation. Note that, -since the client type is an arbitrary string, chains they must not register two light clients which -return the same value for the `ClientType()` function, otherwise the allowlist check can be +The allowed clients parameter defines an allow list of client types supported by the chain. The +default value is a single-element list containing the `AllowAllClients` wildcard (`"*"`). When the +wilcard is used, then all client types are supported by default. Alternatively, the parameter +may be set with a list of client types (e.g. `"06-solomachine","07-tendermint","09-localhost"`). +A client type that is not registered on this list will fail upon creation or on genesis validation. +Note that, since the client type is an arbitrary string, chains must not register two light clients +which return the same value for the `ClientType()` function, otherwise the allow list check can be bypassed. diff --git a/e2e/tests/wasm/grandpa_test.go b/e2e/tests/wasm/grandpa_test.go new file mode 100644 index 00000000000..b67a4c930e7 --- /dev/null +++ b/e2e/tests/wasm/grandpa_test.go @@ -0,0 +1,768 @@ +//go:build !test_e2e + +package wasm + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "testing" + "time" + + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/chain/polkadot" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +const ( + composable = "composable" + simd = "simd" + wasmSimdImage = "ghcr.io/cosmos/ibc-go-wasm-simd" + + defaultWasmClientID = "08-wasm-0" +) + +func TestGrandpaTestSuite(t *testing.T) { + // this test suite only works with the hyperspace relayer, for now hard code this here. + // this will enforce that the hyperspace relayer is used in CI. + t.Setenv(testsuite.RelayerIDEnv, "hyperspace") + + // TODO: this value should be passed in via the config file / CI, not hard coded in the test. + // This configuration can be handled in https://github.com/cosmos/ibc-go/issues/4697 + if testsuite.IsCI() && !testsuite.IsFork() { + t.Setenv(testsuite.ChainImageEnv, wasmSimdImage) + } + + // wasm tests require a longer voting period to account for the time it takes to upload a contract. + testvalues.VotingPeriod = time.Minute * 5 + + validateTestConfig() + testifysuite.Run(t, new(GrandpaTestSuite)) +} + +type GrandpaTestSuite struct { + testsuite.E2ETestSuite +} + +// TestMsgTransfer_Succeeds_GrandpaContract features +// * sets up a Polkadot parachain +// * sets up a Cosmos chain +// * sets up the Hyperspace relayer +// * Funds a user wallet on both chains +// * Pushes a wasm client contract to the Cosmos chain +// * create client, connection, and channel in relayer +// * start relayer +// * send transfer over ibc +func (s *GrandpaTestSuite) TestMsgTransfer_Succeeds_GrandpaContract() { + ctx := context.Background() + t := s.T() + + chainA, chainB := s.GetGrandpaTestChains() + + polkadotChain := chainA.(*polkadot.PolkadotChain) + cosmosChain := chainB.(*cosmos.CosmosChain) + + // we explicitly skip path creation as the contract needs to be uploaded before we can create clients. + r := s.ConfigureRelayer(ctx, polkadotChain, cosmosChain, nil, func(options *interchaintest.InterchainBuildOptions) { + options.SkipPathCreation = true + }) + + s.InitGRPCClients(cosmosChain) + + cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz") + s.Require().NoError(err) + + checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file) + + s.Require().NotEmpty(checksum, "checksum was empty but should not have been") + + eRep := s.GetRelayerExecReporter() + + // Set client contract hash in cosmos chain config + err = r.SetClientContractHash(ctx, eRep, cosmosChain.Config(), checksum) + s.Require().NoError(err) + + // Ensure parachain has started (starts 1 session/epoch after relay chain) + err = testutil.WaitForBlocks(ctx, 1, polkadotChain) + s.Require().NoError(err, "polkadot chain failed to make blocks") + + // Fund users on both cosmos and parachain, mints Asset 1 for Alice + fundAmount := int64(12_333_000_000_000) + polkadotUser, cosmosUser := s.fundUsers(ctx, fundAmount, polkadotChain, cosmosChain) + + // TODO: this can be refactored to broadcast a MsgTransfer instead of CLI. + // https://github.com/cosmos/ibc-go/issues/4963 + amountToSend := int64(1_770_000) + transfer := ibc.WalletAmount{ + Address: polkadotUser.FormattedAddress(), + Denom: cosmosChain.Config().Denom, + Amount: sdkmath.NewInt(amountToSend), + } + + pathName := s.GetPathName(0) + + err = r.GeneratePath(ctx, eRep, cosmosChain.Config().ChainID, polkadotChain.Config().ChainID, pathName) + s.Require().NoError(err) + + // Create new clients + err = r.CreateClients(ctx, eRep, pathName, ibc.DefaultClientOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) // these 1 block waits seem to be needed to reduce flakiness + s.Require().NoError(err) + + // Create a new connection + err = r.CreateConnections(ctx, eRep, pathName) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Create a new channel & get channels from each chain + err = r.CreateChannel(ctx, eRep, pathName, ibc.DefaultChannelOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Start relayer + s.Require().NoError(r.StartRelayer(ctx, eRep, pathName)) + + t.Run("send successful IBC transfer from Cosmos to Polkadot parachain", func(t *testing.T) { + // Send 1.77 stake from cosmosUser to parachainUser + tx, err := cosmosChain.SendIBCTransfer(ctx, "channel-0", cosmosUser.KeyName(), transfer, ibc.TransferOptions{}) + s.Require().NoError(tx.Validate(), "source ibc transfer tx is invalid") + s.Require().NoError(err) + // verify token balance for cosmos user has decreased + balance, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), cosmosChain.Config().Denom) + s.Require().NoError(err) + s.Require().Equal(balance, sdkmath.NewInt(fundAmount-amountToSend), "unexpected cosmos user balance after first tx") + err = testutil.WaitForBlocks(ctx, 15, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Verify tokens arrived on parachain user + parachainUserStake, err := polkadotChain.GetIbcBalance(ctx, string(polkadotUser.Address()), 2) + s.Require().NoError(err) + s.Require().Equal(amountToSend, parachainUserStake.Amount.Int64(), "unexpected parachain user balance after first tx") + }) + + t.Run("send two successful IBC transfers from Polkadot parachain to Cosmos, first with ibc denom, second with parachain denom", func(t *testing.T) { + // Send 1.16 stake from parachainUser to cosmosUser + amountToReflect := int64(1_160_000) + reflectTransfer := ibc.WalletAmount{ + Address: cosmosUser.FormattedAddress(), + Denom: "2", // stake + Amount: sdkmath.NewInt(amountToReflect), + } + _, err := polkadotChain.SendIBCTransfer(ctx, "channel-0", polkadotUser.KeyName(), reflectTransfer, ibc.TransferOptions{}) + s.Require().NoError(err) + + // Send 1.88 "UNIT" from Alice to cosmosUser + amountUnits := sdkmath.NewInt(1_880_000_000_000) + unitTransfer := ibc.WalletAmount{ + Address: cosmosUser.FormattedAddress(), + Denom: "1", // UNIT + Amount: amountUnits, + } + _, err = polkadotChain.SendIBCTransfer(ctx, "channel-0", "alice", unitTransfer, ibc.TransferOptions{}) + s.Require().NoError(err) + + // Wait for MsgRecvPacket on cosmos chain + finalStakeBal := sdkmath.NewInt(fundAmount - amountToSend + amountToReflect) + err = cosmos.PollForBalance(ctx, cosmosChain, 20, ibc.WalletAmount{ + Address: cosmosUser.FormattedAddress(), + Denom: cosmosChain.Config().Denom, + Amount: finalStakeBal, + }) + s.Require().NoError(err) + + // Wait for a new update state + err = testutil.WaitForBlocks(ctx, 5, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Verify cosmos user's final "stake" balance + cosmosUserStakeBal, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), cosmosChain.Config().Denom) + s.Require().NoError(err) + s.Require().True(cosmosUserStakeBal.Equal(finalStakeBal)) + + // Verify cosmos user's final "unit" balance + unitDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", "channel-0", "UNIT")) + cosmosUserUnitBal, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), unitDenomTrace.IBCDenom()) + s.Require().NoError(err) + s.Require().True(cosmosUserUnitBal.Equal(amountUnits)) + + // Verify parachain user's final "unit" balance (will be less than expected due gas costs for stake tx) + parachainUserUnits, err := polkadotChain.GetIbcBalance(ctx, string(polkadotUser.Address()), 1) + s.Require().NoError(err) + s.Require().True(parachainUserUnits.Amount.LTE(sdkmath.NewInt(fundAmount)), "parachain user's final unit amount not expected") + + // Verify parachain user's final "stake" balance + parachainUserStake, err := polkadotChain.GetIbcBalance(ctx, string(polkadotUser.Address()), 2) + s.Require().NoError(err) + s.Require().True(parachainUserStake.Amount.Equal(sdkmath.NewInt(amountToSend-amountToReflect)), "parachain user's final stake amount not expected") + }) +} + +// TestMsgTransfer_TimesOut_GrandpaContract +// sets up cosmos and polkadot chains, hyperspace relayer, and funds users on both chains +// * sends transfer over ibc channel, this transfer should timeout +func (s *GrandpaTestSuite) TestMsgTransfer_TimesOut_GrandpaContract() { + ctx := context.Background() + t := s.T() + + chainA, chainB := s.GetGrandpaTestChains() + + polkadotChain := chainA.(*polkadot.PolkadotChain) + cosmosChain := chainB.(*cosmos.CosmosChain) + + // we explicitly skip path creation as the contract needs to be uploaded before we can create clients. + r := s.ConfigureRelayer(ctx, polkadotChain, cosmosChain, nil, func(options *interchaintest.InterchainBuildOptions) { + options.SkipPathCreation = true + }) + + s.InitGRPCClients(cosmosChain) + + cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz") + s.Require().NoError(err) + + checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file) + + s.Require().NotEmpty(checksum, "checksum was empty but should not have been") + + eRep := s.GetRelayerExecReporter() + + // Set client contract hash in cosmos chain config + err = r.SetClientContractHash(ctx, eRep, cosmosChain.Config(), checksum) + s.Require().NoError(err) + + // Ensure parachain has started (starts 1 session/epoch after relay chain) + err = testutil.WaitForBlocks(ctx, 1, polkadotChain) + s.Require().NoError(err, "polkadot chain failed to make blocks") + + // Fund users on both cosmos and parachain, mints Asset 1 for Alice + fundAmount := int64(12_333_000_000_000) + polkadotUser, cosmosUser := s.fundUsers(ctx, fundAmount, polkadotChain, cosmosChain) + + // TODO: this can be refactored to broadcast a MsgTransfer instead of CLI. + // https://github.com/cosmos/ibc-go/issues/4963 + amountToSend := int64(1_770_000) + transfer := ibc.WalletAmount{ + Address: polkadotUser.FormattedAddress(), + Denom: cosmosChain.Config().Denom, + Amount: sdkmath.NewInt(amountToSend), + } + + pathName := s.GetPathName(0) + + err = r.GeneratePath(ctx, eRep, cosmosChain.Config().ChainID, polkadotChain.Config().ChainID, pathName) + s.Require().NoError(err) + + // Create new clients + err = r.CreateClients(ctx, eRep, pathName, ibc.DefaultClientOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) // these 1 block waits seem to be needed to reduce flakiness + s.Require().NoError(err) + + // Create a new connection + err = r.CreateConnections(ctx, eRep, pathName) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Create a new channel & get channels from each chain + err = r.CreateChannel(ctx, eRep, pathName, ibc.DefaultChannelOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // Start relayer + s.Require().NoError(r.StartRelayer(ctx, eRep, pathName)) + + t.Run("IBC transfer from Cosmos chain to Polkadot parachain times out", func(t *testing.T) { + // Stop relayer + s.Require().NoError(r.StopRelayer(ctx, s.GetRelayerExecReporter())) + + tx, err := cosmosChain.SendIBCTransfer(ctx, "channel-0", cosmosUser.KeyName(), transfer, ibc.TransferOptions{Timeout: testvalues.ImmediatelyTimeout()}) + s.Require().NoError(err) + s.Require().NoError(tx.Validate(), "source ibc transfer tx is invalid") + time.Sleep(time.Nanosecond * 1) // want it to timeout immediately + + // check that tokens are escrowed + actualBalance, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), cosmosChain.Config().Denom) + s.Require().NoError(err) + expected := fundAmount - amountToSend + s.Require().Equal(expected, actualBalance.Int64()) + + // start relayer + s.Require().NoError(r.StartRelayer(ctx, s.GetRelayerExecReporter(), s.GetPathName(0))) + err = testutil.WaitForBlocks(ctx, 15, polkadotChain, cosmosChain) + s.Require().NoError(err) + + // ensure that receiver on parachain did not receive any tokens + receiverBalance, err := polkadotChain.GetIbcBalance(ctx, polkadotUser.FormattedAddress(), 2) + s.Require().NoError(err) + s.Require().Equal(int64(0), receiverBalance.Amount.Int64()) + + // check that tokens have been refunded to sender address + senderBalance, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), cosmosChain.Config().Denom) + s.Require().NoError(err) + s.Require().Equal(fundAmount, senderBalance.Int64()) + }) +} + +// TestMsgMigrateContract_Success_GrandpaContract features +// * sets up a Polkadot parachain +// * sets up a Cosmos chain +// * sets up the Hyperspace relayer +// * Funds a user wallet on both chains +// * Pushes a wasm client contract to the Cosmos chain +// * create client in relayer +// * Pushes a new wasm client contract to the Cosmos chain +// * Migrates the wasm client contract +func (s *GrandpaTestSuite) TestMsgMigrateContract_Success_GrandpaContract() { + ctx := context.Background() + + chainA, chainB := s.GetGrandpaTestChains() + + polkadotChain := chainA.(*polkadot.PolkadotChain) + cosmosChain := chainB.(*cosmos.CosmosChain) + + // we explicitly skip path creation as the contract needs to be uploaded before we can create clients. + r := s.ConfigureRelayer(ctx, polkadotChain, cosmosChain, nil, func(options *interchaintest.InterchainBuildOptions) { + options.SkipPathCreation = true + }) + + s.InitGRPCClients(cosmosChain) + + cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz") + s.Require().NoError(err) + + checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file) + + s.Require().NotEmpty(checksum, "checksum was empty but should not have been") + + eRep := s.GetRelayerExecReporter() + + // Set client contract hash in cosmos chain config + err = r.SetClientContractHash(ctx, eRep, cosmosChain.Config(), checksum) + s.Require().NoError(err) + + // Ensure parachain has started (starts 1 session/epoch after relay chain) + err = testutil.WaitForBlocks(ctx, 1, polkadotChain) + s.Require().NoError(err, "polkadot chain failed to make blocks") + + pathName := s.GetPathName(0) + + err = r.GeneratePath(ctx, eRep, cosmosChain.Config().ChainID, polkadotChain.Config().ChainID, pathName) + s.Require().NoError(err) + + // Create new clients + err = r.CreateClients(ctx, eRep, pathName, ibc.DefaultClientOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) // these 1 block waits seem to be needed to reduce flakiness + s.Require().NoError(err) + + // Do not start relayer + + // This contract is a dummy contract that will always succeed migration. + // Other entry points are unimplemented. + migrateFile, err := os.Open("contracts/migrate_success.wasm.gz") + s.Require().NoError(err) + + // First Store the code + newChecksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, migrateFile) + s.Require().NotEmpty(newChecksum, "checksum was empty but should not have been") + + newChecksumBz, err := hex.DecodeString(newChecksum) + s.Require().NoError(err) + + // Attempt to migrate the contract + message := wasmtypes.NewMsgMigrateContract( + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + defaultWasmClientID, + newChecksumBz, + []byte("{}"), + ) + + s.ExecuteAndPassGovV1Proposal(ctx, message, cosmosChain, cosmosWallet) + + clientState, err := s.QueryClientState(ctx, cosmosChain, defaultWasmClientID) + s.Require().NoError(err) + + wasmClientState, ok := clientState.(*wasmtypes.ClientState) + s.Require().True(ok) + + s.Require().Equal(newChecksumBz, wasmClientState.Checksum) +} + +// TestMsgMigrateContract_ContractError_GrandpaContract features +// * sets up a Polkadot parachain +// * sets up a Cosmos chain +// * sets up the Hyperspace relayer +// * Funds a user wallet on both chains +// * Pushes a wasm client contract to the Cosmos chain +// * create client in relayer +// * Pushes a new wasm client contract to the Cosmos chain +// * Migrates the wasm client contract with a contract that will always fail migration +func (s *GrandpaTestSuite) TestMsgMigrateContract_ContractError_GrandpaContract() { + ctx := context.Background() + + chainA, chainB := s.GetGrandpaTestChains() + + polkadotChain := chainA.(*polkadot.PolkadotChain) + cosmosChain := chainB.(*cosmos.CosmosChain) + + // we explicitly skip path creation as the contract needs to be uploaded before we can create clients. + r := s.ConfigureRelayer(ctx, polkadotChain, cosmosChain, nil, func(options *interchaintest.InterchainBuildOptions) { + options.SkipPathCreation = true + }) + + s.InitGRPCClients(cosmosChain) + + cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz") + s.Require().NoError(err) + checksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file) + + s.Require().NotEmpty(checksum, "checksum was empty but should not have been") + + eRep := s.GetRelayerExecReporter() + + // Set client contract hash in cosmos chain config + err = r.SetClientContractHash(ctx, eRep, cosmosChain.Config(), checksum) + s.Require().NoError(err) + + // Ensure parachain has started (starts 1 session/epoch after relay chain) + err = testutil.WaitForBlocks(ctx, 1, polkadotChain) + s.Require().NoError(err, "polkadot chain failed to make blocks") + + pathName := s.GetPathName(0) + + err = r.GeneratePath(ctx, eRep, cosmosChain.Config().ChainID, polkadotChain.Config().ChainID, pathName) + s.Require().NoError(err) + + // Create new clients + err = r.CreateClients(ctx, eRep, pathName, ibc.DefaultClientOpts()) + s.Require().NoError(err) + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) // these 1 block waits seem to be needed to reduce flakiness + s.Require().NoError(err) + + // Do not start the relayer + + // This contract is a dummy contract that will always fail migration. + // Other entry points are unimplemented. + migrateFile, err := os.Open("contracts/migrate_error.wasm.gz") + s.Require().NoError(err) + + // First Store the code + newChecksum := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, migrateFile) + s.Require().NotEmpty(newChecksum, "checksum was empty but should not have been") + + newChecksumBz, err := hex.DecodeString(newChecksum) + s.Require().NoError(err) + + // Attempt to migrate the contract + message := wasmtypes.NewMsgMigrateContract( + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + defaultWasmClientID, + newChecksumBz, + []byte("{}"), + ) + + err = s.ExecuteGovV1Proposal(ctx, message, cosmosChain, cosmosWallet) + s.Require().Error(err) + + version := cosmosChain.Nodes()[0].Image.Version + if govV1FailedReasonFeatureReleases.IsSupported(version) { + // This is the error string that is returned from the contract + s.Require().ErrorContains(err, "migration not supported") + } +} + +// TestRecoverClient_Succeeds_GrandpaContract features: +// * setup cosmos and polkadot substrates nodes +// * funds test user wallets on both chains +// * stores a wasm client contract on the cosmos chain +// * creates a subject client using the hyperspace relayer +// * waits the expiry period and asserts the subject client status has expired +// * creates a substitute client using the hyperspace relayer +// * executes a gov proposal to recover the expired client +// * asserts the status of the subject client has been restored to active +// NOTE: The testcase features a modified grandpa client contract compiled as: +// - ics10_grandpa_cw_expiry.wasm.gz +// This contract modifies the unbonding period to 1600s with the trusting period being calculated as (unbonding period / 3). +func (s *GrandpaTestSuite) TestRecoverClient_Succeeds_GrandpaContract() { + ctx := context.Background() + + // set the trusting period to a value which will still be valid upon client creation, but invalid before the first update + // the contract uses 1600s as the unbonding period with the trusting period evaluating to (unbonding period / 3) + modifiedTrustingPeriod := (1600 * time.Second) / 3 + + chainA, chainB := s.GetGrandpaTestChains() + + polkadotChain := chainA.(*polkadot.PolkadotChain) + cosmosChain := chainB.(*cosmos.CosmosChain) + + // we explicitly skip path creation as the contract needs to be uploaded before we can create clients. + r := s.ConfigureRelayer(ctx, polkadotChain, cosmosChain, nil, func(options *interchaintest.InterchainBuildOptions) { + options.SkipPathCreation = true + }) + + s.InitGRPCClients(cosmosChain) + + cosmosWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + file, err := os.Open("contracts/ics10_grandpa_cw_expiry.wasm.gz") + s.Require().NoError(err) + + codeHash := s.PushNewWasmClientProposal(ctx, cosmosChain, cosmosWallet, file) + s.Require().NotEmpty(codeHash, "codehash was empty but should not have been") + + eRep := s.GetRelayerExecReporter() + + // Set client contract hash in cosmos chain config + err = r.SetClientContractHash(ctx, eRep, cosmosChain.Config(), codeHash) + s.Require().NoError(err) + + // Ensure parachain has started (starts 1 session/epoch after relay chain) + err = testutil.WaitForBlocks(ctx, 1, polkadotChain) + s.Require().NoError(err, "polkadot chain failed to make blocks") + + // Fund users on both cosmos and parachain, mints Asset 1 for Alice + fundAmount := int64(12_333_000_000_000) + _, cosmosUser := s.fundUsers(ctx, fundAmount, polkadotChain, cosmosChain) + + pathName := s.GetPathName(0) + err = r.GeneratePath(ctx, eRep, cosmosChain.Config().ChainID, polkadotChain.Config().ChainID, pathName) + s.Require().NoError(err) + + // create client pair with subject (bad trusting period) + subjectClientID := clienttypes.FormatClientIdentifier(wasmtypes.Wasm, 0) + // TODO: The hyperspace relayer makes no use of create client opts + // https://github.com/strangelove-ventures/interchaintest/blob/main/relayer/hyperspace/hyperspace_commander.go#L83 + s.SetupClients(ctx, r, ibc.CreateClientOptions{ + TrustingPeriod: modifiedTrustingPeriod.String(), // NOTE: this is hardcoded within the cw contract: ics10_grapnda_cw_expiry.wasm + }) + + // wait for block + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // wait the bad trusting period + time.Sleep(modifiedTrustingPeriod) + + // create client pair with substitute + substituteClientID := clienttypes.FormatClientIdentifier(wasmtypes.Wasm, 1) + s.SetupClients(ctx, r, ibc.DefaultClientOpts()) + + // wait for block + err = testutil.WaitForBlocks(ctx, 1, cosmosChain, polkadotChain) + s.Require().NoError(err) + + // ensure subject client is expired + status, err := s.clientStatus(ctx, cosmosChain, subjectClientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Expired.String(), status, "unexpected subject client status") + + // ensure substitute client is active + status, err = s.clientStatus(ctx, cosmosChain, substituteClientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Active.String(), status, "unexpected substitute client status") + + version := cosmosChain.Nodes()[0].Image.Version + if govV1FeatureReleases.IsSupported(version) { + // create and execute a client recovery proposal + authority, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, cosmosChain) + s.Require().NoError(err) + + msgRecoverClient := clienttypes.NewMsgRecoverClient(authority.String(), subjectClientID, substituteClientID) + s.Require().NotNil(msgRecoverClient) + s.ExecuteAndPassGovV1Proposal(ctx, msgRecoverClient, cosmosChain, cosmosUser) + } else { + proposal := clienttypes.NewClientUpdateProposal(ibctesting.Title, ibctesting.Description, subjectClientID, substituteClientID) + s.ExecuteAndPassGovV1Beta1Proposal(ctx, cosmosChain, cosmosWallet, proposal) + } + + // ensure subject client is active + status, err = s.clientStatus(ctx, cosmosChain, subjectClientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Active.String(), status) + + // ensure substitute client is active + status, err = s.clientStatus(ctx, cosmosChain, substituteClientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Active.String(), status) +} + +// extractChecksumFromGzippedContent takes a gzipped wasm contract and returns the checksum. +func (s *GrandpaTestSuite) extractChecksumFromGzippedContent(zippedContent []byte) string { + content, err := wasmtypes.Uncompress(zippedContent, wasmtypes.MaxWasmByteSize()) + s.Require().NoError(err) + + checksum32 := sha256.Sum256(content) + return hex.EncodeToString(checksum32[:]) +} + +// PushNewWasmClientProposal submits a new wasm client governance proposal to the chain. +func (s *GrandpaTestSuite) PushNewWasmClientProposal(ctx context.Context, chain *cosmos.CosmosChain, wallet ibc.Wallet, proposalContentReader io.Reader) string { + zippedContent, err := io.ReadAll(proposalContentReader) + s.Require().NoError(err) + + computedChecksum := s.extractChecksumFromGzippedContent(zippedContent) + + s.Require().NoError(err) + message := wasmtypes.MsgStoreCode{ + Signer: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + WasmByteCode: zippedContent, + } + + s.ExecuteAndPassGovV1Proposal(ctx, &message, chain, wallet) + + checksumBz, err := s.QueryWasmCode(ctx, chain, computedChecksum) + s.Require().NoError(err) + + checksum32 := sha256.Sum256(checksumBz) + actualChecksum := hex.EncodeToString(checksum32[:]) + s.Require().Equal(computedChecksum, actualChecksum, "checksum returned from query did not match the computed checksum") + + return actualChecksum +} + +func (s *GrandpaTestSuite) clientStatus(ctx context.Context, chain ibc.Chain, clientID string) (string, error) { + queryClient := s.GetChainGRCPClients(chain).ClientQueryClient + res, err := queryClient.ClientStatus(ctx, &clienttypes.QueryClientStatusRequest{ + ClientId: clientID, + }) + if err != nil { + return "", err + } + + return res.Status, nil +} + +func (s *GrandpaTestSuite) fundUsers(ctx context.Context, fundAmount int64, polkadotChain ibc.Chain, cosmosChain ibc.Chain) (ibc.Wallet, ibc.Wallet) { + users := interchaintest.GetAndFundTestUsers(s.T(), ctx, "user", sdkmath.NewInt(fundAmount), polkadotChain, cosmosChain) + polkadotUser, cosmosUser := users[0], users[1] + err := testutil.WaitForBlocks(ctx, 2, polkadotChain, cosmosChain) // Only waiting 1 block is flaky for parachain + s.Require().NoError(err, "cosmos or polkadot chain failed to make blocks") + + // Check balances are correct + amount := sdkmath.NewInt(fundAmount) + polkadotUserAmount, err := polkadotChain.GetBalance(ctx, polkadotUser.FormattedAddress(), polkadotChain.Config().Denom) + s.Require().NoError(err) + s.Require().True(polkadotUserAmount.Equal(amount), "Initial polkadot user amount not expected") + + parachainUserAmount, err := polkadotChain.GetBalance(ctx, polkadotUser.FormattedAddress(), "") + s.Require().NoError(err) + s.Require().True(parachainUserAmount.Equal(amount), "Initial parachain user amount not expected") + + cosmosUserAmount, err := cosmosChain.GetBalance(ctx, cosmosUser.FormattedAddress(), cosmosChain.Config().Denom) + s.Require().NoError(err) + s.Require().True(cosmosUserAmount.Equal(amount), "Initial cosmos user amount not expected") + + return polkadotUser, cosmosUser +} + +// validateTestConfig ensures that the given test config is valid for this test suite. +func validateTestConfig() { + tc := testsuite.LoadConfig() + if tc.ActiveRelayer != "hyperspace" { + panic(fmt.Errorf("hyperspace relayer must be specified")) + } +} + +// getConfigOverrides returns configuration overrides that will be applied to the simapp. +func getConfigOverrides() map[string]any { + consensusOverrides := make(testutil.Toml) + blockTime := 5 + blockT := (time.Duration(blockTime) * time.Second).String() + consensusOverrides["timeout_commit"] = blockT + consensusOverrides["timeout_propose"] = blockT + + configTomlOverrides := make(testutil.Toml) + configTomlOverrides["consensus"] = consensusOverrides + configTomlOverrides["log_level"] = "info" + + configFileOverrides := make(map[string]any) + configFileOverrides["config/config.toml"] = configTomlOverrides + return configFileOverrides +} + +// GetGrandpaTestChains returns the configured chains for the grandpa test suite. +func (s *GrandpaTestSuite) GetGrandpaTestChains() (ibc.Chain, ibc.Chain) { + return s.GetChains(func(options *testsuite.ChainOptions) { + // configure chain A (polkadot) + options.ChainASpec.ChainName = composable + options.ChainASpec.Type = "polkadot" + options.ChainASpec.ChainID = "rococo-local" + options.ChainASpec.Name = "composable" + options.ChainASpec.Images = []ibc.DockerImage{ + // TODO: https://github.com/cosmos/ibc-go/issues/4965 + { + Repository: "ghcr.io/misko9/polkadot-node", + Version: "v39", + UidGid: "1000:1000", + }, + { + Repository: "ghcr.io/misko9/parachain-node", + Version: "20231122v39", + UidGid: "1000:1000", + }, + } + options.ChainASpec.Bin = "polkadot" + options.ChainASpec.Bech32Prefix = composable + options.ChainASpec.Denom = "uDOT" + options.ChainASpec.GasPrices = "" + options.ChainASpec.GasAdjustment = 0 + options.ChainASpec.TrustingPeriod = "" + options.ChainASpec.CoinType = "354" + + // these values are set by default for our cosmos chains, we need to explicitly remove them here. + options.ChainASpec.ModifyGenesis = nil + options.ChainASpec.ConfigFileOverrides = nil + options.ChainASpec.EncodingConfig = nil + + // configure chain B (cosmos) + options.ChainBSpec.ChainName = simd // Set chain name so that a suffix with a "dash" is not appended (required for hyperspace) + options.ChainBSpec.Type = "cosmos" + options.ChainBSpec.Name = "simd" + options.ChainBSpec.ChainID = simd + options.ChainBSpec.Bin = simd + options.ChainBSpec.Bech32Prefix = "cosmos" + + // TODO: hyperspace relayer assumes a denom of "stake", hard code this here right now. + // https://github.com/cosmos/ibc-go/issues/4964 + options.ChainBSpec.Denom = "stake" + options.ChainBSpec.GasPrices = "0.00stake" + options.ChainBSpec.GasAdjustment = 1 + options.ChainBSpec.TrustingPeriod = "504h" + options.ChainBSpec.CoinType = "118" + + options.ChainBSpec.ChainConfig.NoHostMount = false + options.ChainBSpec.ConfigFileOverrides = getConfigOverrides() + options.ChainBSpec.EncodingConfig = testsuite.SDKEncodingConfig() + }) +} diff --git a/e2e/testsuite/testconfig.go b/e2e/testsuite/testconfig.go new file mode 100644 index 00000000000..f8cfe1b5b7a --- /dev/null +++ b/e2e/testsuite/testconfig.go @@ -0,0 +1,725 @@ +package testsuite + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + interchaintestutil "github.com/strangelove-ventures/interchaintest/v8/testutil" + "gopkg.in/yaml.v2" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + cmtjson "github.com/cometbft/cometbft/libs/json" + + "github.com/cosmos/ibc-go/e2e/relayer" + "github.com/cosmos/ibc-go/e2e/semverutil" + "github.com/cosmos/ibc-go/e2e/testvalues" + wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctypes "github.com/cosmos/ibc-go/v8/modules/core/types" +) + +const ( + // ChainImageEnv specifies the image that the chains will use. If left unspecified, it will + // default to being determined based on the specified binary. E.g. ghcr.io/cosmos/ibc-go-simd + ChainImageEnv = "CHAIN_IMAGE" + // ChainATagEnv specifies the tag that Chain A will use. + ChainATagEnv = "CHAIN_A_TAG" + // ChainBTagEnv specifies the tag that Chain B will use. If unspecified + // the value will default to the same value as Chain A. + ChainBTagEnv = "CHAIN_B_TAG" + // RelayerIDEnv specifies the ID of the relayer to use. + RelayerIDEnv = "RELAYER_ID" + // ChainBinaryEnv binary is the binary that will be used for both chains. + ChainBinaryEnv = "CHAIN_BINARY" + // ChainUpgradeTagEnv specifies the upgrade version tag + ChainUpgradeTagEnv = "CHAIN_UPGRADE_TAG" + // ChainUpgradePlanEnv specifies the upgrade plan name + ChainUpgradePlanEnv = "CHAIN_UPGRADE_PLAN" + // E2EConfigFilePathEnv allows you to specify a custom path for the config file to be used. + E2EConfigFilePathEnv = "E2E_CONFIG_PATH" + + // defaultBinary is the default binary that will be used by the chains. + defaultBinary = "simd" + // defaultRlyTag is the tag that will be used if no relayer tag is specified. + // all images are here https://github.com/cosmos/relayer/pkgs/container/relayer/versions + defaultRlyTag = "latest" + + // TODO: https://github.com/cosmos/ibc-go/issues/4965 + defaultHyperspaceTag = "20231122v39" + // defaultHermesTag is the tag that will be used if no relayer tag is specified for hermes. + defaultHermesTag = "luca_joss-channel-upgrade-authority" + // defaultChainTag is the tag that will be used for the chains if none is specified. + defaultChainTag = "main" + // defaultConfigFileName is the default filename for the config file that can be used to configure + // e2e tests. See sample.config.yaml as an example for what this should look like. + defaultConfigFileName = ".ibc-go-e2e-config.yaml" +) + +func getChainImage(binary string) string { + if binary == "" { + binary = defaultBinary + } + return fmt.Sprintf("ghcr.io/cosmos/ibc-go-%s", binary) +} + +// TestConfig holds configuration used throughout the different e2e tests. +type TestConfig struct { + // ChainConfigs holds configuration values related to the chains used in the tests. + ChainConfigs []ChainConfig `yaml:"chains"` + // RelayerConfig holds all known relayer configurations that can be used in the tests. + RelayerConfigs []relayer.Config `yaml:"relayers"` + // ActiveRelayer specifies the relayer that will be used. It must match the ID of one of the entries in RelayerConfigs. + ActiveRelayer string `yaml:"activeRelayer"` + // UpgradeConfig holds values used only for the upgrade tests. + UpgradeConfig UpgradeConfig `yaml:"upgrade"` + // CometBFTConfig holds values for configuring CometBFT. + CometBFTConfig CometBFTConfig `yaml:"cometbft"` + // DebugConfig holds configuration for miscellaneous options. + DebugConfig DebugConfig `yaml:"debug"` +} + +// Validate validates the test configuration is valid for use within the tests. +// this should be called before using the configuration. +func (tc TestConfig) Validate() error { + if err := tc.validateChains(); err != nil { + return fmt.Errorf("invalid chain configuration: %w", err) + } + + if err := tc.validateRelayers(); err != nil { + return fmt.Errorf("invalid relayer configuration: %w", err) + } + return nil +} + +// validateChains validates the chain configurations. +func (tc TestConfig) validateChains() error { + for _, cfg := range tc.ChainConfigs { + if cfg.Binary == "" { + return fmt.Errorf("chain config missing binary: %+v", cfg) + } + if cfg.Image == "" { + return fmt.Errorf("chain config missing image: %+v", cfg) + } + if cfg.Tag == "" { + return fmt.Errorf("chain config missing tag: %+v", cfg) + } + + // TODO: validate chainID in https://github.com/cosmos/ibc-go/issues/4697 + // these are not passed in the CI at the moment. Defaults are used. + if !IsCI() { + if cfg.ChainID == "" { + return fmt.Errorf("chain config missing chainID: %+v", cfg) + } + } + + // TODO: validate number of nodes in https://github.com/cosmos/ibc-go/issues/4697 + // these are not passed in the CI at the moment. + if !IsCI() { + if cfg.NumValidators == 0 && cfg.NumFullNodes == 0 { + return fmt.Errorf("chain config missing number of validators or full nodes: %+v", cfg) + } + } + } + return nil +} + +// validateRelayers validates relayer configuration. +func (tc TestConfig) validateRelayers() error { + if len(tc.RelayerConfigs) < 1 { + return fmt.Errorf("no relayer configurations specified") + } + + for _, r := range tc.RelayerConfigs { + if r.ID == "" { + return fmt.Errorf("relayer config missing ID: %+v", r) + } + if r.Image == "" { + return fmt.Errorf("relayer config missing image: %+v", r) + } + if r.Tag == "" { + return fmt.Errorf("relayer config missing tag: %+v", r) + } + } + + if tc.GetActiveRelayerConfig() == nil { + return fmt.Errorf("active relayer %s not found in relayer configs: %+v", tc.ActiveRelayer, tc.RelayerConfigs) + } + + return nil +} + +// GetActiveRelayerConfig returns the currently specified relayer config. +func (tc TestConfig) GetActiveRelayerConfig() *relayer.Config { + for _, r := range tc.RelayerConfigs { + if r.ID == tc.ActiveRelayer { + return &r + } + } + return nil +} + +// GetChainNumValidators returns the number of validators for the specific chain index. +// default 1 +func (tc TestConfig) GetChainNumValidators(idx int) int { + if tc.ChainConfigs[idx].NumValidators > 0 { + return tc.ChainConfigs[idx].NumValidators + } + return 1 +} + +// GetChainNumFullNodes returns the number of full nodes for the specific chain index. +// default 0 +func (tc TestConfig) GetChainNumFullNodes(idx int) int { + if tc.ChainConfigs[idx].NumFullNodes > 0 { + return tc.ChainConfigs[idx].NumFullNodes + } + return 0 +} + +// GetChainAID returns the chain-id for chain A. +func (tc TestConfig) GetChainAID() string { + if tc.ChainConfigs[0].ChainID != "" { + return tc.ChainConfigs[0].ChainID + } + return "chainA-1" +} + +// GetChainBID returns the chain-id for chain B. +func (tc TestConfig) GetChainBID() string { + if tc.ChainConfigs[1].ChainID != "" { + return tc.ChainConfigs[1].ChainID + } + return "chainB-1" +} + +// UpgradeConfig holds values relevant to upgrade tests. +type UpgradeConfig struct { + PlanName string `yaml:"planName"` + Tag string `yaml:"tag"` +} + +// ChainConfig holds information about an individual chain used in the tests. +type ChainConfig struct { + ChainID string `yaml:"chainId"` + Image string `yaml:"image"` + Tag string `yaml:"tag"` + Binary string `yaml:"binary"` + NumValidators int `yaml:"numValidators"` + NumFullNodes int `yaml:"numFullNodes"` +} + +type CometBFTConfig struct { + LogLevel string `yaml:"logLevel"` +} + +type DebugConfig struct { + // DumpLogs forces the logs to be collected before removing test containers. + DumpLogs bool `yaml:"dumpLogs"` +} + +// LoadConfig attempts to load a atest configuration from the default file path. +// if any environment variables are specified, they will take precedence over the individual configuration +// options. +func LoadConfig() TestConfig { + tc := getConfig() + if err := tc.Validate(); err != nil { + panic(err) + } + return tc +} + +// getConfig returns the TestConfig with any environment variable overrides. +func getConfig() TestConfig { + fileTc, foundFile := fromFile() + if !foundFile { + return fromEnv() + } + + return applyEnvironmentVariableOverrides(fileTc) +} + +// fromFile returns a TestConfig from a json file and a boolean indicating if the file was found. +func fromFile() (TestConfig, bool) { + var tc TestConfig + bz, err := os.ReadFile(getConfigFilePath()) + if err != nil { + return TestConfig{}, false + } + + if err := yaml.Unmarshal(bz, &tc); err != nil { + panic(err) + } + + return tc, true +} + +// applyEnvironmentVariableOverrides applies all environment variable changes to the config +// loaded from a file. +func applyEnvironmentVariableOverrides(fromFile TestConfig) TestConfig { + envTc := fromEnv() + + if os.Getenv(ChainATagEnv) != "" { + fromFile.ChainConfigs[0].Tag = envTc.ChainConfigs[0].Tag + } + + if os.Getenv(ChainBTagEnv) != "" { + fromFile.ChainConfigs[1].Tag = envTc.ChainConfigs[1].Tag + } + + if os.Getenv(ChainBinaryEnv) != "" { + for i := range fromFile.ChainConfigs { + fromFile.ChainConfigs[i].Binary = envTc.ChainConfigs[i].Binary + } + } + + if os.Getenv(ChainImageEnv) != "" { + for i := range fromFile.ChainConfigs { + fromFile.ChainConfigs[i].Image = envTc.ChainConfigs[i].Image + } + } + + if os.Getenv(RelayerIDEnv) != "" { + fromFile.ActiveRelayer = envTc.ActiveRelayer + } + + if os.Getenv(ChainUpgradePlanEnv) != "" { + fromFile.UpgradeConfig.PlanName = envTc.UpgradeConfig.PlanName + } + + if os.Getenv(ChainUpgradeTagEnv) != "" { + fromFile.UpgradeConfig.Tag = envTc.UpgradeConfig.Tag + } + + return fromFile +} + +// fromEnv returns a TestConfig constructed from environment variables. +func fromEnv() TestConfig { + return TestConfig{ + ChainConfigs: getChainConfigsFromEnv(), + UpgradeConfig: getUpgradePlanConfigFromEnv(), + ActiveRelayer: os.Getenv(RelayerIDEnv), + + // TODO: we can remove this, and specify these values in a config file for the CI + // in https://github.com/cosmos/ibc-go/issues/4697 + RelayerConfigs: []relayer.Config{ + getDefaultRlyRelayerConfig(), + getDefaultHermesRelayerConfig(), + getDefaultHyperspaceRelayerConfig(), + }, + CometBFTConfig: CometBFTConfig{LogLevel: "info"}, + } +} + +// getChainConfigsFromEnv returns the chain configs from environment variables. +func getChainConfigsFromEnv() []ChainConfig { + chainBinary, ok := os.LookupEnv(ChainBinaryEnv) + if !ok { + chainBinary = defaultBinary + } + + chainATag, ok := os.LookupEnv(ChainATagEnv) + if !ok { + chainATag = defaultChainTag + } + + chainBTag, ok := os.LookupEnv(ChainBTagEnv) + if !ok { + chainBTag = chainATag + } + + chainAImage := getChainImage(chainBinary) + specifiedChainImage, ok := os.LookupEnv(ChainImageEnv) + if ok { + chainAImage = specifiedChainImage + } + + numValidators := 4 + numFullNodes := 1 + + chainBImage := chainAImage + return []ChainConfig{ + { + Image: chainAImage, + Tag: chainATag, + Binary: chainBinary, + NumValidators: numValidators, + NumFullNodes: numFullNodes, + }, + { + Image: chainBImage, + Tag: chainBTag, + Binary: chainBinary, + NumValidators: numValidators, + NumFullNodes: numFullNodes, + }, + } +} + +// getConfigFilePath returns the absolute path where the e2e config file should be. +func getConfigFilePath() string { + if absoluteConfigPath := os.Getenv(E2EConfigFilePathEnv); absoluteConfigPath != "" { + return absoluteConfigPath + } + + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + return path.Join(homeDir, defaultConfigFileName) +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultHermesRelayerConfig returns the default config for the hermes relayer. +func getDefaultHermesRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultHermesTag, + ID: relayer.Hermes, + Image: relayer.HermesRelayerRepository, + } +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultRlyRelayerConfig returns the default config for the golang relayer. +func getDefaultRlyRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultRlyTag, + ID: relayer.Rly, + Image: relayer.RlyRelayerRepository, + } +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultHyperspaceRelayerConfig returns the default config for the hyperspace relayer. +func getDefaultHyperspaceRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultHyperspaceTag, + ID: relayer.Hyperspace, + Image: relayer.HyperspaceRelayerRepository, + } +} + +// getUpgradePlanConfigFromEnv returns the upgrade config from environment variables. +func getUpgradePlanConfigFromEnv() UpgradeConfig { + upgradeTag, ok := os.LookupEnv(ChainUpgradeTagEnv) + if !ok { + upgradeTag = "" + } + + upgradePlan, ok := os.LookupEnv(ChainUpgradePlanEnv) + if !ok { + upgradePlan = "" + } + return UpgradeConfig{ + PlanName: upgradePlan, + Tag: upgradeTag, + } +} + +func GetChainATag() string { + return LoadConfig().ChainConfigs[0].Tag +} + +func GetChainBTag() string { + if chainBTag := LoadConfig().ChainConfigs[1].Tag; chainBTag != "" { + return chainBTag + } + return GetChainATag() +} + +// IsCI returns true if the tests are running in CI, false is returned +// if the tests are running locally. +// Note: github actions passes a CI env value of true by default to all runners. +func IsCI() bool { + return strings.ToLower(os.Getenv("CI")) == "true" +} + +// IsFork returns true if the tests are running in fork mode, false is returned otherwise. +func IsFork() bool { + return strings.ToLower(os.Getenv("FORK")) == "true" +} + +// ChainOptions stores chain configurations for the chains that will be +// created for the tests. They can be modified by passing ChainOptionConfiguration +// to E2ETestSuite.GetChains. +type ChainOptions struct { + ChainASpec *interchaintest.ChainSpec + ChainBSpec *interchaintest.ChainSpec + SkipPathCreation bool +} + +// ChainOptionConfiguration enables arbitrary configuration of ChainOptions. +type ChainOptionConfiguration func(options *ChainOptions) + +// DefaultChainOptions returns the default configuration for the chains. +// These options can be configured by passing configuration functions to E2ETestSuite.GetChains. +func DefaultChainOptions() ChainOptions { + tc := LoadConfig() + + chainACfg := newDefaultSimappConfig(tc.ChainConfigs[0], "simapp-a", tc.GetChainAID(), "atoma", tc.CometBFTConfig) + chainBCfg := newDefaultSimappConfig(tc.ChainConfigs[1], "simapp-b", tc.GetChainBID(), "atomb", tc.CometBFTConfig) + + chainAVal, chainAFn := getValidatorsAndFullNodes(0) + chainBVal, chainBFn := getValidatorsAndFullNodes(1) + + return ChainOptions{ + ChainASpec: &interchaintest.ChainSpec{ + ChainConfig: chainACfg, + NumFullNodes: &chainAFn, + NumValidators: &chainAVal, + }, + ChainBSpec: &interchaintest.ChainSpec{ + ChainConfig: chainBCfg, + NumFullNodes: &chainBFn, + NumValidators: &chainBVal, + }, + } +} + +// newDefaultSimappConfig creates an ibc configuration for simd. +func newDefaultSimappConfig(cc ChainConfig, name, chainID, denom string, cometCfg CometBFTConfig) ibc.ChainConfig { + configFileOverrides := make(map[string]any) + tmTomlOverrides := make(interchaintestutil.Toml) + + tmTomlOverrides["log_level"] = cometCfg.LogLevel // change to debug in ~/.ibc-go-e2e-config.json to increase cometbft logging. + configFileOverrides["config/config.toml"] = tmTomlOverrides + + return ibc.ChainConfig{ + Type: "cosmos", + Name: name, + ChainID: chainID, + Images: []ibc.DockerImage{ + { + Repository: cc.Image, + Version: cc.Tag, + UidGid: "1000:1000", + }, + }, + Bin: cc.Binary, + Bech32Prefix: "cosmos", + CoinType: fmt.Sprint(sdk.GetConfig().GetCoinType()), + Denom: denom, + EncodingConfig: SDKEncodingConfig(), + GasPrices: fmt.Sprintf("0.00%s", denom), + GasAdjustment: 1.3, + TrustingPeriod: "508h", + NoHostMount: false, + ModifyGenesis: getGenesisModificationFunction(cc), + ConfigFileOverrides: configFileOverrides, + } +} + +// getGenesisModificationFunction returns a genesis modification function that handles the GenesisState type +// correctly depending on if the govv1beta1 gov module is used or if govv1 is being used. +func getGenesisModificationFunction(cc ChainConfig) func(ibc.ChainConfig, []byte) ([]byte, error) { + binary := cc.Binary + version := cc.Tag + + simdSupportsGovV1Genesis := binary == defaultBinary && testvalues.GovGenesisFeatureReleases.IsSupported(version) + + if simdSupportsGovV1Genesis { + return defaultGovv1ModifyGenesis(version) + } + + return defaultGovv1Beta1ModifyGenesis(version) +} + +// defaultGovv1ModifyGenesis will only modify governance params to ensure the voting period and minimum deposit +// are functional for e2e testing purposes. +func defaultGovv1ModifyGenesis(version string) func(ibc.ChainConfig, []byte) ([]byte, error) { + stdlibJSONMarshalling := semverutil.FeatureReleases{MajorVersion: "v8"} + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + appGenesis, err := genutiltypes.AppGenesisFromReader(bytes.NewReader(genbz)) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into genesis doc: %w", err) + } + + var appState genutiltypes.AppMap + if err := json.Unmarshal(appGenesis.AppState, &appState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into app state: %w", err) + } + + govGenBz, err := modifyGovV1AppState(chainConfig, appState[govtypes.ModuleName]) + if err != nil { + return nil, err + } + appState[govtypes.ModuleName] = govGenBz + + if !testvalues.AllowAllClientsWildcardFeatureReleases.IsSupported(version) { + ibcGenBz, err := modifyClientGenesisAppState(chainConfig, appState[ibcexported.ModuleName]) + if err != nil { + return nil, err + } + appState[ibcexported.ModuleName] = ibcGenBz + } + + appGenesis.AppState, err = json.Marshal(appState) + if err != nil { + return nil, err + } + + // in older version < v8, tmjson marshal must be used. + // regular json marshalling must be used for v8 and above as the + // sdk is de-coupled from comet. + marshalIndentFn := cmtjson.MarshalIndent + if stdlibJSONMarshalling.IsSupported(version) { + marshalIndentFn = json.MarshalIndent + } + + bz, err := marshalIndentFn(appGenesis, "", " ") + if err != nil { + return nil, err + } + + return bz, nil + } +} + +// defaultGovv1Beta1ModifyGenesis will only modify governance params to ensure the voting period and minimum deposit +// // are functional for e2e testing purposes. +func defaultGovv1Beta1ModifyGenesis(version string) func(ibc.ChainConfig, []byte) ([]byte, error) { + const appStateKey = "app_state" + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + genesisDocMap := map[string]interface{}{} + err := json.Unmarshal(genbz, &genesisDocMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into genesis doc: %w", err) + } + + appStateMap, ok := genesisDocMap[appStateKey].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to extract to app_state") + } + + govModuleBytes, err := json.Marshal(appStateMap[govtypes.ModuleName]) + if err != nil { + return nil, fmt.Errorf("failed to extract gov genesis bytes: %s", err) + } + + govModuleGenesisBytes, err := modifyGovv1Beta1AppState(chainConfig, govModuleBytes) + if err != nil { + return nil, err + } + + govModuleGenesisMap := map[string]interface{}{} + err = json.Unmarshal(govModuleGenesisBytes, &govModuleGenesisMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gov genesis bytes into map: %w", err) + } + + if !testvalues.AllowAllClientsWildcardFeatureReleases.IsSupported(version) { + ibcModuleBytes, err := json.Marshal(appStateMap[ibcexported.ModuleName]) + if err != nil { + return nil, fmt.Errorf("failed to extract ibc genesis bytes: %s", err) + } + + ibcGenesisBytes, err := modifyClientGenesisAppState(chainConfig, ibcModuleBytes) + if err != nil { + return nil, err + } + + ibcModuleGenesisMap := map[string]interface{}{} + err = json.Unmarshal(ibcGenesisBytes, &ibcModuleGenesisMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gov genesis bytes into map: %w", err) + } + } + + appStateMap[govtypes.ModuleName] = govModuleGenesisMap + genesisDocMap[appStateKey] = appStateMap + + finalGenesisDocBytes, err := json.MarshalIndent(genesisDocMap, "", " ") + if err != nil { + return nil, err + } + + return finalGenesisDocBytes, nil + } +} + +// modifyGovV1AppState takes the existing gov app state and marshals it to a govv1 GenesisState. +func modifyGovV1AppState(chainConfig ibc.ChainConfig, govAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + govv1.RegisterInterfaces(cfg.InterfaceRegistry) + + govGenesisState := &govv1.GenesisState{} + + if err := cdc.UnmarshalJSON(govAppState, govGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into gov genesis state: %w", err) + } + + if govGenesisState.Params == nil { + govGenesisState.Params = &govv1.Params{} + } + + govGenesisState.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + maxDep := time.Second * 10 + govGenesisState.Params.MaxDepositPeriod = &maxDep + vp := testvalues.VotingPeriod + govGenesisState.Params.VotingPeriod = &vp + + govGenBz := MustProtoMarshalJSON(govGenesisState) + + return govGenBz, nil +} + +// modifyGovv1Beta1AppState takes the existing gov app state and marshals it to a govv1beta1 GenesisState. +func modifyGovv1Beta1AppState(chainConfig ibc.ChainConfig, govAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) + + govGenesisState := &govv1beta1.GenesisState{} + if err := cdc.UnmarshalJSON(govAppState, govGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into govv1beta1 genesis state: %w", err) + } + + govGenesisState.DepositParams.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + govGenesisState.VotingParams.VotingPeriod = testvalues.VotingPeriod + + govGenBz, err := cdc.MarshalJSON(govGenesisState) + if err != nil { + return nil, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + + return govGenBz, nil +} + +// modifyClientGenesisAppState takes the existing ibc app state and marshals it to a ibc GenesisState. +func modifyClientGenesisAppState(chainConfig ibc.ChainConfig, ibcAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + clienttypes.RegisterInterfaces(cfg.InterfaceRegistry) + + ibcGenesisState := &ibctypes.GenesisState{} + if err := cdc.UnmarshalJSON(ibcAppState, ibcGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into client genesis state: %w", err) + } + + ibcGenesisState.ClientGenesis.Params.AllowedClients = append(ibcGenesisState.ClientGenesis.Params.AllowedClients, wasmtypes.Wasm) + ibcGenBz, err := cdc.MarshalJSON(ibcGenesisState) + if err != nil { + return nil, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + + return ibcGenBz, nil +} diff --git a/e2e/testvalues/values.go b/e2e/testvalues/values.go new file mode 100644 index 00000000000..bab1aee577e --- /dev/null +++ b/e2e/testvalues/values.go @@ -0,0 +1,101 @@ +package testvalues + +import ( + "fmt" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/ibc" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/e2e/semverutil" + feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" +) + +const ( + StartingTokenAmount int64 = 500_000_000_000 + IBCTransferAmount int64 = 10_000 + InvalidAddress string = "" + DefaultGovV1ProposalTokenAmount = 500_000_000 +) + +// VotingPeriod may differ per test. +var VotingPeriod = time.Second * 30 + +// ImmediatelyTimeout returns an ibc.IBCTimeout which will cause an IBC transfer to timeout immediately. +func ImmediatelyTimeout() *ibc.IBCTimeout { + return &ibc.IBCTimeout{ + NanoSeconds: 1, + } +} + +func DefaultFee(denom string) feetypes.Fee { + return feetypes.Fee{ + RecvFee: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(50))), + AckFee: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(25))), + TimeoutFee: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(10))), + } +} + +func DefaultTransferAmount(denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(IBCTransferAmount)} +} + +func TransferAmount(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} +} + +func TendermintClientID(id int) string { + return fmt.Sprintf("07-tendermint-%d", id) +} + +func SolomachineClientID(id int) string { + return fmt.Sprintf("06-solomachine-%d", id) +} + +// TokenMetadataFeatureReleases represents the releases the token metadata was released in. +var TokenMetadataFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v8", +} + +// GovGenesisFeatureReleases represents the releases the governance module genesis +// was upgraded from v1beta1 to v1. +var GovGenesisFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v7", +} + +// SelfParamsFeatureReleases represents the releases the transfer module started managing its own params. +var SelfParamsFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v8", +} + +// TotalEscrowFeatureReleases represents the releases the total escrow state entry was released in. +var TotalEscrowFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v8", + MinorVersions: []string{ + "v7.1", + }, +} + +// IbcErrorsFeatureReleases represents the releases the IBC module level errors was released in. +var IbcErrorsFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v8", +} + +// LocalhostClientFeatureReleases represents the releases the localhost client was released in. +var LocalhostClientFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v8", + MinorVersions: []string{ + "v7.1", + }, +} + +// AllowAllClientsWildcardFeatureReleases represents the releases the allow all clients wildcard was released in. +var AllowAllClientsWildcardFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v9", + MinorVersions: []string{ + "v8.1", + }, +} diff --git a/modules/core/02-client/types/keys.go b/modules/core/02-client/types/keys.go index 86b132c5195..119fdc728d3 100644 --- a/modules/core/02-client/types/keys.go +++ b/modules/core/02-client/types/keys.go @@ -28,6 +28,10 @@ const ( // ParamsKey is the store key for the IBC client parameters ParamsKey = "clientParams" + + // AllowAllClients is the value that if set in AllowedClients param + // would allow any wired up light client modules to be allowed + AllowAllClients = "*" ) // FormatClientIdentifier returns the client identifier with the sequence appended. diff --git a/modules/core/02-client/types/params.go b/modules/core/02-client/types/params.go index 2234f049e66..e6b4714c668 100644 --- a/modules/core/02-client/types/params.go +++ b/modules/core/02-client/types/params.go @@ -4,12 +4,11 @@ import ( "fmt" "slices" "strings" - - "github.com/cosmos/ibc-go/v8/modules/core/exported" ) // DefaultAllowedClients are the default clients for the AllowedClients parameter. -var DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint, exported.Localhost} +// By default it allows all client types. +var DefaultAllowedClients = []string{AllowAllClients} // NewParams creates a new parameter configuration for the ibc client module func NewParams(allowedClients ...string) Params { @@ -30,11 +29,33 @@ func (p Params) Validate() error { // IsAllowedClient checks if the given client type is registered on the allowlist. func (p Params) IsAllowedClient(clientType string) bool { + // Still need to check for blank client type + if strings.TrimSpace(clientType) == "" { + return false + } + + // Check for allow all client wildcard + // If exist then allow all type of client + if len(p.AllowedClients) == 1 && p.AllowedClients[0] == AllowAllClients { + return true + } + return slices.Contains(p.AllowedClients, clientType) } +<<<<<<< HEAD // validateClients checks that the given clients are not blank. func validateClients(clients []string) error { +======= +// validateClients checks that the given clients are not blank and there are no duplicates. +// If AllowAllClients wildcard (*) is used, then there should no other client types in the allow list +func validateClients(clients []string) error { + if slices.Contains(clients, AllowAllClients) && len(clients) > 1 { + return fmt.Errorf("allow list must have only one element because the allow all clients wildcard (%s) is present", AllowAllClients) + } + + foundClients := make(map[string]bool, len(clients)) +>>>>>>> d5949b1a (feat(statemachine)!: Add allow all client wildcard to AllowedClients param (#5429)) for i, clientType := range clients { if strings.TrimSpace(clientType) == "" { return fmt.Errorf("client type %d cannot be blank", i) diff --git a/modules/core/02-client/types/params_test.go b/modules/core/02-client/types/params_test.go index 404118c0528..15397df231d 100644 --- a/modules/core/02-client/types/params_test.go +++ b/modules/core/02-client/types/params_test.go @@ -19,6 +19,8 @@ func TestIsAllowedClient(t *testing.T) { {"success: valid client with custom params", exported.Tendermint, NewParams(exported.Tendermint), true}, {"success: invalid blank client", " ", DefaultParams(), false}, {"success: invalid client with custom params", exported.Localhost, NewParams(exported.Tendermint), false}, + {"success: wildcard allow all clients", "test-client-type", NewParams(AllowAllClients), true}, + {"success: wildcard allow all clients with blank client", " ", NewParams(AllowAllClients), false}, } for _, tc := range testCases { @@ -36,6 +38,11 @@ func TestValidateParams(t *testing.T) { {"default params", DefaultParams(), true}, {"custom params", NewParams(exported.Tendermint), true}, {"blank client", NewParams(" "), false}, +<<<<<<< HEAD +======= + {"duplicate clients", NewParams(exported.Tendermint, exported.Tendermint), false}, + {"allow all clients plus valid client", NewParams(AllowAllClients, exported.Tendermint), false}, +>>>>>>> d5949b1a (feat(statemachine)!: Add allow all client wildcard to AllowedClients param (#5429)) } for _, tc := range testCases { diff --git a/modules/light-clients/08-wasm/keeper/keeper_test.go b/modules/light-clients/08-wasm/keeper/keeper_test.go new file mode 100644 index 00000000000..73810c74b42 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/keeper_test.go @@ -0,0 +1,326 @@ +package keeper_test + +import ( + "encoding/json" + "errors" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + dbm "github.com/cosmos/cosmos-db" + testifysuite "github.com/stretchr/testify/suite" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/runtime" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing/simapp" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +const ( + defaultWasmClientID = "08-wasm-0" +) + +type KeeperTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + + // mockVM is a mock wasm VM that implements the WasmEngine interface + mockVM *wasmtesting.MockWasmEngine + chainA *ibctesting.TestChain +} + +func init() { + ibctesting.DefaultTestingAppInit = setupTestingApp +} + +// setupTestingApp provides the duplicated simapp which is specific to the 08-wasm module on chain creation. +func setupTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, nil) + return app, app.DefaultGenesis() +} + +// GetSimApp returns the duplicated SimApp from within the 08-wasm directory. +// This must be used instead of chain.GetSimApp() for tests within this directory. +func GetSimApp(chain *ibctesting.TestChain) *simapp.SimApp { + app, ok := chain.App.(*simapp.SimApp) + if !ok { + panic(errors.New("chain is not a simapp.SimApp")) + } + return app +} + +func (suite *KeeperTestSuite) SetupTest() { + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + + queryHelper := baseapp.NewQueryServerTestHelper(suite.chainA.GetContext(), GetSimApp(suite.chainA).InterfaceRegistry()) + types.RegisterQueryServer(queryHelper, GetSimApp(suite.chainA).WasmClientKeeper) +} + +// SetupWasmWithMockVM sets up mock cometbft chain with a mock vm. +func (suite *KeeperTestSuite) SetupWasmWithMockVM() { + ibctesting.DefaultTestingAppInit = suite.setupWasmWithMockVM + + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func (suite *KeeperTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[string]json.RawMessage) { + suite.mockVM = wasmtesting.NewMockWasmEngine() + + suite.mockVM.InstantiateFn = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var payload types.InstantiateMessage + err := json.Unmarshal(initMsg, &payload) + suite.Require().NoError(err) + + wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState) + + clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.GetLatestHeight().(clienttypes.Height)) + clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) + store.Set(host.ClientStateKey(), clientStateBz) + + consensusState := types.NewConsensusState(payload.ConsensusState) + consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState) + store.Set(host.ConsensusStateKey(clientState.GetLatestHeight()), consensusStateBz) + + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: resp}, 0, nil + } + + 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) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Active.String()}) + suite.Require().NoError(err) + return resp, wasmtesting.DefaultGasUsed, nil + }) + + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, suite.mockVM) + + // reset DefaultTestingAppInit to its original value + ibctesting.DefaultTestingAppInit = setupTestingApp + return app, app.DefaultGenesis() +} + +// storeWasmCode stores the wasm code on chain and returns the checksum. +func storeWasmCode(suite *KeeperTestSuite, wasmCode []byte) []byte { + ctx := suite.chainA.GetContext().WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + + msg := types.NewMsgStoreCode(authtypes.NewModuleAddress(govtypes.ModuleName).String(), wasmCode) + response, err := GetSimApp(suite.chainA).WasmClientKeeper.StoreCode(ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(response.Checksum) + return response.Checksum +} + +func (suite *KeeperTestSuite) SetupSnapshotterWithMockVM() *simapp.SimApp { + suite.mockVM = wasmtesting.NewMockWasmEngine() + + return simapp.SetupWithSnapshotter(suite.T(), suite.mockVM) +} + +func TestKeeperTestSuite(t *testing.T) { + testifysuite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) TestNewKeeper() { + testCases := []struct { + name string + instantiateFn func() + expPass bool + expError error + }{ + { + "success", + 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(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + ) + }, + true, + nil, + }, + { + "failure: empty authority", + func() { + keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + "", // authority + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + ) + }, + false, + errors.New("authority must be non-empty"), + }, + { + "failure: nil client keeper", + func() { + keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), + nil, // client keeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + GetSimApp(suite.chainA).GRPCQueryRouter(), + ) + }, + false, + errors.New("client keeper must be not nil"), + }, + { + "failure: nil wasm VM", + 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(), + nil, + GetSimApp(suite.chainA).GRPCQueryRouter(), + ) + }, + false, + errors.New("wasm VM must be not nil"), + }, + { + "failure: nil store service", + func() { + keeper.NewKeeperWithVM( + GetSimApp(suite.chainA).AppCodec(), + nil, + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, + GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), + ibcwasm.GetVM(), + 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 { + tc := tc + suite.SetupTest() + + suite.Run(tc.name, func() { + if tc.expPass { + suite.Require().NotPanics( + tc.instantiateFn, + ) + } else { + suite.Require().PanicsWithError(tc.expError.Error(), func() { + tc.instantiateFn() + }) + } + }) + } +} + +func (suite *KeeperTestSuite) TestInitializedPinnedCodes() { + var capturedChecksums []wasmvm.Checksum + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + suite.mockVM.PinFn = func(checksum wasmvm.Checksum) error { + capturedChecksums = append(capturedChecksums, checksum) + return nil + } + }, + nil, + }, + { + "failure: pin error", + func() { + suite.mockVM.PinFn = func(checksum wasmvm.Checksum) error { + return wasmtesting.ErrMockVM + } + }, + wasmtesting.ErrMockVM, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + ctx := suite.chainA.GetContext() + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + + contracts := [][]byte{wasmtesting.Code, wasmtesting.CreateMockContract([]byte("gzipped-contract"))} + checksumIDs := make([]types.Checksum, len(contracts)) + signer := authtypes.NewModuleAddress(govtypes.ModuleName).String() + + // store contract on chain + for i, contract := range contracts { + msg := types.NewMsgStoreCode(signer, contract) + + res, err := wasmClientKeeper.StoreCode(ctx, msg) + suite.Require().NoError(err) + + checksumIDs[i] = res.Checksum + } + + // malleate after storing contracts + tc.malleate() + + err := keeper.InitializePinnedCodes(ctx) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + suite.ElementsMatch(checksumIDs, capturedChecksums) + } else { + suite.Require().ErrorIs(err, tc.expError) + } + }) + } +} diff --git a/modules/light-clients/08-wasm/testing/wasm_endpoint.go b/modules/light-clients/08-wasm/testing/wasm_endpoint.go new file mode 100644 index 00000000000..6d126ea35c9 --- /dev/null +++ b/modules/light-clients/08-wasm/testing/wasm_endpoint.go @@ -0,0 +1,52 @@ +package testing + +import ( + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// WasmEndpoint is a wrapper around the ibctesting pkg Endpoint struct. +// It will override any functions which require special handling for the wasm client. +type WasmEndpoint struct { + *ibctesting.Endpoint +} + +// NewWasmEndpoint returns a wasm endpoint with the default ibctesting pkg +// Endpoint embedded. +func NewWasmEndpoint(chain *ibctesting.TestChain) *WasmEndpoint { + return &WasmEndpoint{ + Endpoint: ibctesting.NewDefaultEndpoint(chain), + } +} + +// CreateClient creates an wasm client on a mock cometbft chain. +// The client and consensus states are represented by byte slices +// and the starting height is 1. +func (endpoint *WasmEndpoint) CreateClient() error { + checksum, err := types.CreateChecksum(Code) + require.NoError(endpoint.Chain.TB, err) + + wrappedClientStateBz := clienttypes.MustMarshalClientState(endpoint.Chain.App.AppCodec(), CreateMockTendermintClientState(clienttypes.NewHeight(1, 5))) + wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(endpoint.Chain.App.AppCodec(), MockTendermintClientConsensusState) + + clientState := types.NewClientState(wrappedClientStateBz, checksum, clienttypes.NewHeight(0, 1)) + consensusState := types.NewConsensusState(wrappedClientConsensusStateBz) + + msg, err := clienttypes.NewMsgCreateClient( + clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.TB, err) + + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.Events) + require.NoError(endpoint.Chain.TB, err) + + return nil +} diff --git a/modules/light-clients/08-wasm/types/types_test.go b/modules/light-clients/08-wasm/types/types_test.go new file mode 100644 index 00000000000..e1729f66997 --- /dev/null +++ b/modules/light-clients/08-wasm/types/types_test.go @@ -0,0 +1,131 @@ +package types_test + +import ( + "encoding/json" + "errors" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + dbm "github.com/cosmos/cosmos-db" + testifysuite "github.com/stretchr/testify/suite" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + simapp "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing/simapp" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +const ( + tmClientID = "07-tendermint-0" + defaultWasmClientID = "08-wasm-0" +) + +type TypesTestSuite struct { + testifysuite.Suite + coordinator *ibctesting.Coordinator + chainA *ibctesting.TestChain + mockVM *wasmtesting.MockWasmEngine + + checksum types.Checksum +} + +func TestWasmTestSuite(t *testing.T) { + testifysuite.Run(t, new(TypesTestSuite)) +} + +func (suite *TypesTestSuite) SetupTest() { + ibctesting.DefaultTestingAppInit = setupTestingApp + + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func init() { + ibctesting.DefaultTestingAppInit = setupTestingApp +} + +// GetSimApp returns the duplicated SimApp from within the 08-wasm directory. +// This must be used instead of chain.GetSimApp() for tests within this directory. +func GetSimApp(chain *ibctesting.TestChain) *simapp.SimApp { + app, ok := chain.App.(*simapp.SimApp) + if !ok { + panic(errors.New("chain is not a simapp.SimApp")) + } + return app +} + +// setupTestingApp provides the duplicated simapp which is specific to the 08-wasm module on chain creation. +func setupTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, nil) + return app, app.DefaultGenesis() +} + +// SetupWasmWithMockVM sets up mock cometbft chain with a mock vm. +func (suite *TypesTestSuite) SetupWasmWithMockVM() { + ibctesting.DefaultTestingAppInit = suite.setupWasmWithMockVM + + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.checksum = storeWasmCode(suite, wasmtesting.Code) +} + +func (suite *TypesTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[string]json.RawMessage) { + suite.mockVM = wasmtesting.NewMockWasmEngine() + + suite.mockVM.InstantiateFn = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var payload types.InstantiateMessage + err := json.Unmarshal(initMsg, &payload) + suite.Require().NoError(err) + + wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState) + + clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.GetLatestHeight().(clienttypes.Height)) + clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) + store.Set(host.ClientStateKey(), clientStateBz) + + consensusState := types.NewConsensusState(payload.ConsensusState) + consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState) + store.Set(host.ConsensusStateKey(clientState.GetLatestHeight()), consensusStateBz) + + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: resp}, 0, nil + } + + 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) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Active.String()}) + suite.Require().NoError(err) + return resp, wasmtesting.DefaultGasUsed, nil + }) + + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, suite.mockVM) + + // reset DefaultTestingAppInit to its original value + ibctesting.DefaultTestingAppInit = setupTestingApp + return app, app.DefaultGenesis() +} + +// storeWasmCode stores the wasm code on chain and returns the checksum. +func storeWasmCode(suite *TypesTestSuite, wasmCode []byte) types.Checksum { + ctx := suite.chainA.GetContext().WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + + msg := types.NewMsgStoreCode(authtypes.NewModuleAddress(govtypes.ModuleName).String(), wasmCode) + response, err := GetSimApp(suite.chainA).WasmClientKeeper.StoreCode(ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(response.Checksum) + return response.Checksum +}