diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml index b596f22a9ce..79415380b5c 100644 --- a/.github/workflows/e2e-upgrade.yaml +++ b/.github/workflows/e2e-upgrade.yaml @@ -41,3 +41,15 @@ jobs: upgrade-plan-name: "v7" test-entry-point: "TestUpgradeTestSuite" test: "TestV6ToV7ChainUpgrade" + + upgrade-v7_1: + uses: cosmos/ibc-go/.github/workflows/e2e-test-workflow-call.yml@main + with: + chain-image: ghcr.io/cosmos/ibc-go-simd + chain-binary: simd + chain-a-tag: pr-3136 # TODO: needs v7.0.0 (with simapp fixes) when cut + chain-b-tag: pr-3136 # TODO: needs v7.0.0 (with simapp fixes) when cut + chain-upgrade-tag: pr-3164 # TODO: needs v7.1.0 when cut + upgrade-plan-name: "v7.1" + test-entry-point: "TestUpgradeTestSuite" + test: "TestV7ChainUpgradeAddLocalhost" diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index ba11c556e9e..90e172b6cf4 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -215,6 +215,11 @@ module.exports = { directory: false, path: "/roadmap/roadmap.html", }, + { + title: "Troubleshooting", + directory: false, + path: "/ibc/troubleshooting.html", + }, ], }, { @@ -393,6 +398,38 @@ module.exports = { }, ] }, + { + title: "Localhost", + directory: true, + path: "/ibc/light-clients/localhost", + children: [ + { + title: "Overview", + directory: false, + path: "/ibc/light-clients/localhost/overview.html", + }, + { + title: "Integration", + directory: false, + path: "/ibc/light-clients/localhost/integration.html", + }, + { + title: "ClientState", + directory: false, + path: "/ibc/light-clients/localhost/client-state.html", + }, + { + title: "Connection", + directory: false, + path: "/ibc/light-clients/localhost/connection.html", + }, + { + title: "State Verification", + directory: false, + path: "/ibc/light-clients/localhost/state-verification.html", + }, + ], + }, { title: "Solomachine", directory: true, @@ -508,6 +545,11 @@ module.exports = { directory: false, path: "/migrations/v6-to-v7.html", }, + { + title: "IBC-Go v7 to v7.1", + directory: false, + path: "/migrations/v7-to-v7_1.html", + }, ], }, { diff --git a/docs/ibc/events.md b/docs/ibc/events.md index 080c284deaa..d01c1b37649 100644 --- a/docs/ibc/events.md +++ b/docs/ibc/events.md @@ -185,63 +185,90 @@ callbacks to IBC applications. ### SendPacket (application module call) -| Type | Attribute Key | Attribute Value | -|-------------|--------------------------|----------------------------------| -| send_packet | packet_data | {data} | -| send_packet | packet_timeout_height | {timeoutHeight} | -| send_packet | packet_timeout_timestamp | {timeoutTimestamp} | -| send_packet | packet_sequence | {sequence} | -| send_packet | packet_src_port | {sourcePort} | -| send_packet | packet_src_channel | {sourceChannel} | -| send_packet | packet_dst_port | {destinationPort} | -| send_packet | packet_dst_channel | {destinationChannel} | -| send_packet | packet_channel_ordering | {channel.Ordering} | -| message | action | application-module-defined-field | -| message | module | ibc-channel | +| Type | Attribute Key | Attribute Value | Status | +|-------------|--------------------------|----------------------------------|------------| +| send_packet | packet_data | {data} | Deprecated | +| send_packet | packet_data_hex | {hex.Encode(data)} | | +| send_packet | packet_timeout_height | {timeoutHeight} | | +| send_packet | packet_timeout_timestamp | {timeoutTimestamp} | | +| send_packet | packet_sequence | {sequence} | | +| send_packet | packet_src_port | {sourcePort} | | +| send_packet | packet_src_channel | {sourceChannel} | | +| send_packet | packet_dst_port | {destinationPort} | | +| send_packet | packet_dst_channel | {destinationChannel} | | +| send_packet | packet_channel_ordering | {channel.Ordering} | | +| send_packet | packet_connection | {channel.ConnectionHops[0]} | Deprecated | +| send_packet | connection_id | {channel.ConnectionHops[0]} | | +| message | action | application-module-defined-field | | +| message | module | ibc-channel | | ### MsgRecvPacket -| Type | Attribute Key | Attribute Value | -|-------------|--------------------------|----------------------| -| recv_packet | packet_data | {data} | -| recv_packet | packet_ack | {acknowledgement} | -| recv_packet | packet_timeout_height | {timeoutHeight} | -| recv_packet | packet_timeout_timestamp | {timeoutTimestamp} | -| recv_packet | packet_sequence | {sequence} | -| recv_packet | packet_src_port | {sourcePort} | -| recv_packet | packet_src_channel | {sourceChannel} | -| recv_packet | packet_dst_port | {destinationPort} | -| recv_packet | packet_dst_channel | {destinationChannel} | -| recv_packet | packet_channel_ordering | {channel.Ordering} | -| message | action | recv_packet | -| message | module | ibc-channel | +| Type | Attribute Key | Attribute Value | Status | +|-------------|--------------------------|-------------------------------|------------| +| recv_packet | packet_data | {data} | Deprecated | +| recv_packet | packet_data_hex | {hex.Encode(data)} | | +| recv_packet | packet_timeout_height | {timeoutHeight} | | +| recv_packet | packet_timeout_timestamp | {timeoutTimestamp} | | +| recv_packet | packet_sequence | {sequence} | | +| recv_packet | packet_src_port | {sourcePort} | | +| recv_packet | packet_src_channel | {sourceChannel} | | +| recv_packet | packet_dst_port | {destinationPort} | | +| recv_packet | packet_dst_channel | {destinationChannel} | | +| recv_packet | packet_channel_ordering | {channel.Ordering} | | +| recv_packet | packet_connection | {channel.ConnectionHops[0]} | Deprecated | +| recv_packet | connection_id | {channel.ConnectionHops[0]} | | +| message | action | recv_packet | | +| message | module | ibc-channel | | + +| Type | Attribute Key | Attribute Value | Status | +|-----------------------|--------------------------|-------------------------------|------------| +| write_acknowledgement | packet_data | {data} | Deprecated | +| write_acknowledgement | packet_data_hex | {hex.Encode(data)} | | +| write_acknowledgement | packet_timeout_height | {timeoutHeight} | | +| write_acknowledgement | packet_timeout_timestamp | {timeoutTimestamp} | | +| write_acknowledgement | packet_sequence | {sequence} | | +| write_acknowledgement | packet_src_port | {sourcePort} | | +| write_acknowledgement | packet_src_channel | {sourceChannel} | | +| write_acknowledgement | packet_dst_port | {destinationPort} | | +| write_acknowledgement | packet_dst_channel | {destinationChannel} | | +| write_acknowledgement | packet_ack | {ack} | Deprecated | +| write_acknowledgement | packet_ack_hex | {hex.Encode(ack)} | | +| write_acknowledgement | packet_channel_ordering | {channel.Ordering} | | +| write_acknowledgement | packet_connection | {channel.ConnectionHops[0]} | Deprecated | +| write_acknowledgement | connection_id | {channel.ConnectionHops[0]} | | +| message | action | write_acknowledgement | | +| message | module | ibc-channel | | ### MsgAcknowledgePacket -| Type | Attribute Key | Attribute Value | -|--------------------|--------------------------|----------------------| -| acknowledge_packet | packet_timeout_height | {timeoutHeight} | -| acknowledge_packet | packet_timeout_timestamp | {timeoutTimestamp} | -| acknowledge_packet | packet_sequence | {sequence} | -| acknowledge_packet | packet_src_port | {sourcePort} | -| acknowledge_packet | packet_src_channel | {sourceChannel} | -| acknowledge_packet | packet_dst_port | {destinationPort} | -| acknowledge_packet | packet_dst_channel | {destinationChannel} | -| acknowledge_packet | packet_channel_ordering | {channel.Ordering} | -| message | action | acknowledge_packet | -| message | module | ibc-channel | +| Type | Attribute Key | Attribute Value | Status | +|--------------------|--------------------------|-------------------------------|------------| +| acknowledge_packet | packet_timeout_height | {timeoutHeight} | | +| acknowledge_packet | packet_timeout_timestamp | {timeoutTimestamp} | | +| acknowledge_packet | packet_sequence | {sequence} | | +| acknowledge_packet | packet_src_port | {sourcePort} | | +| acknowledge_packet | packet_src_channel | {sourceChannel} | | +| acknowledge_packet | packet_dst_port | {destinationPort} | | +| acknowledge_packet | packet_dst_channel | {destinationChannel} | | +| acknowledge_packet | packet_channel_ordering | {channel.Ordering} | | +| acknowledge_packet | packet_connection | {channel.ConnectionHops[0]} | Deprecated | +| acknowledge_packet | connection_id | {channel.ConnectionHops[0]} | | +| message | action | acknowledge_packet | | +| message | module | ibc-channel | | ### MsgTimeoutPacket & MsgTimeoutOnClose -| Type | Attribute Key | Attribute Value | -|----------------|--------------------------|----------------------| -| timeout_packet | packet_timeout_height | {timeoutHeight} | -| timeout_packet | packet_timeout_timestamp | {timeoutTimestamp} | -| timeout_packet | packet_sequence | {sequence} | -| timeout_packet | packet_src_port | {sourcePort} | -| timeout_packet | packet_src_channel | {sourceChannel} | -| timeout_packet | packet_dst_port | {destinationPort} | -| timeout_packet | packet_dst_channel | {destinationChannel} | -| timeout_packet | packet_channel_ordering | {channel.Ordering} | -| message | action | timeout_packet | -| message | module | ibc-channel | +| Type | Attribute Key | Attribute Value | +|----------------|--------------------------|-------------------------------| +| timeout_packet | packet_timeout_height | {timeoutHeight} | +| timeout_packet | packet_timeout_timestamp | {timeoutTimestamp} | +| timeout_packet | packet_sequence | {sequence} | +| timeout_packet | packet_src_port | {sourcePort} | +| timeout_packet | packet_src_channel | {sourceChannel} | +| timeout_packet | packet_dst_port | {destinationPort} | +| timeout_packet | packet_dst_channel | {destinationChannel} | +| timeout_packet | packet_channel_ordering | {channel.Ordering} | +| timeout_packet | connection_id | {channel.ConnectionHops[0]} | +| message | action | timeout_packet | +| message | module | ibc-channel | diff --git a/docs/ibc/light-clients/localhost/client-state.md b/docs/ibc/light-clients/localhost/client-state.md new file mode 100644 index 00000000000..15c6e1c4b8a --- /dev/null +++ b/docs/ibc/light-clients/localhost/client-state.md @@ -0,0 +1,60 @@ + + +# `ClientState` + +The 09-localhost `ClientState` maintains a single field used to track the latest sequence of the state machine i.e. the height of the blockchain. + +```go +type ClientState struct { + // the latest height of the blockchain + LatestHeight clienttypes.Height +} +``` + +The 09-localhost `ClientState` is instantiated in the `InitGenesis` handler of the 02-client submodule in core IBC. +It calls `CreateLocalhostClient`, declaring a new `ClientState` and initializing it with its own client prefixed store. + +```go +func (k Keeper) CreateLocalhostClient(ctx sdk.Context) error { + var clientState localhost.ClientState + return clientState.Initialize(ctx, k.cdc, k.ClientStore(ctx, exported.LocalhostClientID), nil) +} +``` + +It is possible to disable the localhost client by removing the `09-localhost` entry from the `allowed_clients` list through governance. + +## Client updates + +The latest height is updated periodically through the ABCI [`BeginBlock`](https://docs.cosmos.network/v0.47/building-modules/beginblock-endblock) interface of the 02-client submodule in core IBC. + +[See `BeginBlocker` in abci.go.](https://github.com/cosmos/ibc-go/blob/09-localhost/modules/core/02-client/abci.go#L12) + +```go +func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { + // ... + + if clientState, found := k.GetClientState(ctx, exported.Localhost); found { + if k.GetClientStatus(ctx, clientState, exported.Localhost) == exported.Active { + k.UpdateLocalhostClient(ctx, clientState) + } + } +} +``` + +The above calls into the the 09-localhost `UpdateState` method of the `ClientState` . +It retrieves the current block height from the application context and sets the `LatestHeight` of the 09-localhost client. + +```go +func (cs ClientState) UpdateState(ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, clientMsg exported.ClientMessage) []exported.Height { + height := clienttypes.GetSelfHeight(ctx) + cs.LatestHeight = height + + clientStore.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(cdc, &cs)) + + return []exported.Height{height} +} +``` + +Note that the 09-localhost `ClientState` is not updated through the 02-client interface leveraged by conventional IBC light clients. diff --git a/docs/ibc/light-clients/localhost/connection.md b/docs/ibc/light-clients/localhost/connection.md new file mode 100644 index 00000000000..33251bde432 --- /dev/null +++ b/docs/ibc/light-clients/localhost/connection.md @@ -0,0 +1,25 @@ + + +# Localhost connections + +The 09-localhost light client module integrates with core IBC through a single sentinel localhost connection. +The sentinel `ConnectionEnd` is stored by default in the core IBC store. + +This enables channel handshakes to be initiated out of the box by supplying the localhost connection identifier (`connection-localhost`) in the `connectionHops` parameter of `MsgChannelOpenInit`. + +The `ConnectionEnd` is created and set in store via the `InitGenesis` handler of the 03-connection submodule in core IBC. +The `ConnectionEnd` and its `Counterparty` both reference the `09-localhost` client identifier, and share the localhost connection identifier `connection-localhost`. + +```go +// CreateSentinelLocalhostConnection creates and sets the sentinel localhost connection end in the IBC store. +func (k Keeper) CreateSentinelLocalhostConnection(ctx sdk.Context) { + counterparty := types.NewCounterparty(exported.LocalhostClientID, exported.LocalhostConnectionID, commitmenttypes.NewMerklePrefix(k.GetCommitmentPrefix().Bytes())) + connectionEnd := types.NewConnectionEnd(types.OPEN, exported.LocalhostClientID, counterparty, types.ExportedVersionsToProto(types.GetCompatibleVersions()), 0) + + k.SetConnection(ctx, exported.LocalhostConnectionID, connectionEnd) +} +``` + +Note that connection handshakes are disallowed when using the `09-localhost` client type. diff --git a/docs/ibc/light-clients/localhost/integration.md b/docs/ibc/light-clients/localhost/integration.md new file mode 100644 index 00000000000..a818507f1fd --- /dev/null +++ b/docs/ibc/light-clients/localhost/integration.md @@ -0,0 +1,16 @@ + + +# 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-rc0/proto/ibc/core/client/v1/client.proto#L102) by default in ibc-go. + +```go +var ( + // DefaultAllowedClients are the default clients for the AllowedClients parameter. + DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint, exported.Localhost} +) +``` \ No newline at end of file diff --git a/docs/ibc/light-clients/localhost/overview.md b/docs/ibc/light-clients/localhost/overview.md new file mode 100644 index 00000000000..eca758e0221 --- /dev/null +++ b/docs/ibc/light-clients/localhost/overview.md @@ -0,0 +1,40 @@ + + +# `09-localhost` + +## Overview + +Learn about the 09-localhost light client module. {synopsis} + +The 09-localhost light client module implements a localhost loopback client with the ability to send and receive IBC packets to and from the same state machine. + +### Context + +In a multichain environment, application developers will be used to developing cross-chain applications through IBC. From their point of view, whether or not they are interacting with multiple modules on the same chain or on different chains should not matter. The localhost client module enables a unified interface to interact with different applications on a single chain, using the familiar IBC application layer semantics. + +### Implementation + +There exists a [single sentinel `ClientState`](./client-state.md) instance with the client identifier `09-localhost`. + +To supplement this, a [sentinel `ConnectionEnd` is stored in core IBC](./connection.md) state with the connection identifier `connection-localhost`. This enables IBC applications to create channels directly on top of the sentinel connection which leverage the 09-localhost loopback functionality. + +[State verification](./state-verification.md) for channel state in handshakes or processing packets is reduced in complexity, the `09-localhost` client can simply compare bytes stored under the standardized key paths. + +### Localhost vs *regular* client + +The localhost client aims to provide a unified approach to interacting with applications on a single chain, as the IBC application layer provides for cross-chain interactions. To achieve this unified interface though, there are a number of differences under the hood compared to a 'regular' IBC client (excluding `06-solomachine` and `09-localhost` itself). + +The table below lists some important differences: + +| | Regular client | Localhost | +| -------------------------------------------- | --------------------------------------------------------------------------- | --------- | +| Number of clients | Many instances of a client *type* corresponding to different counterparties | A single sentinel client with the client identifier `09-localhost`| +| Client creation | Relayer (permissionless) | `ClientState` is instantiated in the `InitGenesis` handler of the 02-client submodule in core IBC | +| Client updates | Relayer submits headers using `MsgUpdateClient` | Latest height is updated periodically through the ABCI [`BeginBlock`](https://docs.cosmos.network/v0.47/building-modules/beginblock-endblock) interface of the 02-client submodule in core IBC | +| Number of connections | Many connections, 1 (or more) per client | A single sentinel connection with the connection identifier `connection-localhost` | +| Connection creation | Connection handshake, provided underlying client | Sentinel `ConnectionEnd` is created and set in store in the `InitGenesis` handler of the 03-connection submodule in core IBC | +| Counterparty | Underlying client, representing another chain | Client with identifier `09-localhost` in same chain | +| `VerifyMembership` and `VerifyNonMembership` | Performs proof verification using consensus state roots | Performs state verification using key-value lookups in the core IBC store | +| Integration | Expected to register codec types using the `AppModuleBasic` interface | Registers codec types within the core IBC module | diff --git a/docs/ibc/light-clients/localhost/state-verification.md b/docs/ibc/light-clients/localhost/state-verification.md new file mode 100644 index 00000000000..a208c8734c3 --- /dev/null +++ b/docs/ibc/light-clients/localhost/state-verification.md @@ -0,0 +1,18 @@ + + +# State verification + +The localhost client handles state verification through the `ClientState` interface methods `VerifyMembership` and `VerifyNonMembership` by performing read-only operations directly on the core IBC store. + +When verifying channel state in handshakes or processing packets the `09-localhost` client can simply compare bytes stored under the standardized key paths defined by [ICS-24](https://github.com/cosmos/ibc/tree/main/spec/core/ics-024-host-requirements). + +For existence proofs via `VerifyMembership` the 09-localhost client will retrieve the value stored under the provided key path and compare it against the value provided by the caller. In contrast, non-existence proofs via `VerifyNonMembership` assert the absence of a value at the provided key path. + +Relayers are expected to provide a sentinel proof when sending IBC messages. Submission of nil or empty proofs is disallowed in core IBC messaging. +The 09-localhost light client module defines a `SentinelProof` as a single byte. Localhost client state verification will fail if the sentintel proof value is not provided. + +```go +var SentinelProof = []byte{0x01} +``` diff --git a/docs/ibc/params.md b/docs/ibc/params.md index 3040eea50b7..dcf60692695 100644 --- a/docs/ibc/params.md +++ b/docs/ibc/params.md @@ -10,7 +10,7 @@ The 02-client submodule contains the following parameters: | Key | Type | Default Value | |------------------|------|---------------| -| `AllowedClients` | []string | `"06-solomachine","07-tendermint"` | +| `AllowedClients` | []string | `"06-solomachine","07-tendermint","09-localhost"` | ### AllowedClients diff --git a/docs/ibc/troubleshooting.md b/docs/ibc/troubleshooting.md new file mode 100644 index 00000000000..95576752e73 --- /dev/null +++ b/docs/ibc/troubleshooting.md @@ -0,0 +1,8 @@ +# Troubleshooting + +### Unauthorized client states + +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. diff --git a/docs/migrations/v7-to-v7_1.md b/docs/migrations/v7-to-v7_1.md new file mode 100644 index 00000000000..c01c8a05375 --- /dev/null +++ b/docs/migrations/v7-to-v7_1.md @@ -0,0 +1,58 @@ +# Migrating from v7 to v7.1 + +This guide provides instructions for migrating to version `v7.1.0` of ibc-go. + +There are four sections based on the four potential user groups of this document: + +- [Migrating from v7 to v7.1](#migrating-from-v7-to-v71) + - [Chains](#chains) + - [IBC Apps](#ibc-apps) + - [Relayers](#relayers) + - [IBC Light Clients](#ibc-light-clients) + +**Note:** ibc-go supports golang semantic versioning and therefore all imports must be updated on major version releases. + +## Chains + +In the previous release of ibc-go, the localhost `v1` light client module was deprecated and removed. The ibc-go `v7.1.0` release introduces `v2` of the 09-localhost light client module. + + +An [automatic migration handler](https://github.com/cosmos/ibc-go/blob/09-localhost/modules/core/module.go#L133-L145) is configured in the core IBC module to set the localhost `ClientState` and sentintel `ConnectionEnd` in state. + +In order to use the 09-localhost client chains must update the `AllowedClients` parameter in the 02-client submodule of core IBC. This can be configured directly in the application upgrade handler or alternatively updated via the legacy governance parameter change proposal. +We __strongly__ recommend chains to perform this action. + +See the upgrade handler code sample provided below or [follow this link](https://github.com/cosmos/ibc-go/blob/09-localhost/testing/simapp/upgrades/upgrades.go#L85) for the upgrade handler used by the ibc-go simapp. + +```go +func CreateV7LocalhostUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + clientKeeper clientkeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + // explicitly update the IBC 02-client params, adding the localhost client type + params := clientKeeper.GetParams(ctx) + params.AllowedClients = append(params.AllowedClients, exported.Localhost) + clientKeeper.SetParams(ctx, params) + + return mm.RunMigrations(ctx, configurator, vm) + } +} +``` + +[For more information please refer to the 09-localhost light client module documentation](../ibc/light-clients/localhost/overview.md). + +## IBC Apps + +- No relevant changes were made in this release. + +## Relayers + +The event attribute `packet_connection` (`connectiontypes.AttributeKeyConnection`) has been deprecated. +Please use the `connection_id` attribute (`connectiontypes.AttributeKeyConnectionID`) which is emitted by all channel events. +Only send packet, receive packet, write acknowledgement, and acknowledge packet events used `packet_connection` previously. + +## IBC Light Clients + +- No relevant changes were made in this release. diff --git a/e2e/relayer/relayer.go b/e2e/relayer/relayer.go index 7467a9f47f3..f96af34a7c9 100644 --- a/e2e/relayer/relayer.go +++ b/e2e/relayer/relayer.go @@ -15,8 +15,8 @@ const ( Rly = "rly" Hermes = "hermes" - cosmosRelayerRepository = "ghcr.io/cosmos/relayer" - cosmosRelayerUser = "100:1000" // docker run -it --rm --entrypoint echo ghcr.io/cosmos/relayer "$(id -u):$(id -g)" + cosmosRelayerRepository = "damiannolan/rly" //"ghcr.io/cosmos/relayer" + cosmosRelayerUser = "100:1000" // docker run -it --rm --entrypoint echo ghcr.io/cosmos/relayer "$(id -u):$(id -g)" ) // Config holds configuration values for the relayer used in the tests. diff --git a/e2e/testconfig/testconfig.go b/e2e/testconfig/testconfig.go index 48261fedce9..088fbd3b7bf 100644 --- a/e2e/testconfig/testconfig.go +++ b/e2e/testconfig/testconfig.go @@ -46,7 +46,7 @@ const ( 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 = "andrew-tendermint_v0.37" // "v2.2.0" + defaultRlyTag = "latest" // "andrew-tendermint_v0.37" // "v2.2.0" // defaultChainTag is the tag that will be used for the chains if none is specified. defaultChainTag = "main" // defaultRelayerType is the default relayer that will be used if none is specified. diff --git a/e2e/tests/interchain_accounts/localhost_test.go b/e2e/tests/interchain_accounts/localhost_test.go new file mode 100644 index 00000000000..1b374b3ff8e --- /dev/null +++ b/e2e/tests/interchain_accounts/localhost_test.go @@ -0,0 +1,497 @@ +package interchain_accounts + +import ( + "context" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/gogoproto/proto" + controllertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" + ibctesting "github.com/cosmos/ibc-go/v7/testing" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + "github.com/strangelove-ventures/interchaintest/v7" + "github.com/strangelove-ventures/interchaintest/v7/ibc" + test "github.com/strangelove-ventures/interchaintest/v7/testutil" +) + +func TestInterchainAccountsLocalhostTestSuite(t *testing.T) { + suite.Run(t, new(LocalhostInterchainAccountsTestSuite)) +} + +type LocalhostInterchainAccountsTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *LocalhostInterchainAccountsTestSuite) TestInterchainAccounts_Localhost() { + t := s.T() + ctx := context.TODO() + + _, _ = s.SetupChainsRelayerAndChannel(ctx) + chainA, _ := s.GetChains() + + chainADenom := chainA.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userBWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + msgChanOpenInitRes channeltypes.MsgChannelOpenInitResponse + msgChanOpenTryRes channeltypes.MsgChannelOpenTryResponse + ack []byte + packet channeltypes.Packet + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks") + + version := icatypes.NewDefaultMetadataString(exported.LocalhostConnectionID, exported.LocalhostConnectionID) + controllerPortID, err := icatypes.NewControllerPortID(userAWallet.FormattedAddress()) + s.Require().NoError(err) + + t.Run("channel open init localhost - broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.GetChannelId(), + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.GetChannelId(), + msgChanOpenTryRes.GetChannelId(), msgChanOpenTryRes.GetVersion(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.GetChannelId(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account registration and deposit funds", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + walletAmount := ibc.WalletAmount{ + Address: interchainAccAddress, + Amount: testvalues.StartingTokenAmount, + Denom: chainADenom, + } + + s.Require().NoError(chainA.SendFunds(ctx, interchaintest.FaucetAccountKeyName, walletAmount)) + }) + + t.Run("send packet localhost interchain accounts", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertValidTxResponse(txResp) + s.Require().NoError(err) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + packet, err = ibctesting.ParsePacketFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("recv packet localhost interchain accounts", func(t *testing.T) { + msgRecvPacket := channeltypes.NewMsgRecvPacket(packet, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgRecvPacket) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + ack, err = ibctesting.ParseAckFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(ack) + }) + + t.Run("acknowledge packet localhost interchain accounts", func(t *testing.T) { + msgAcknowledgement := channeltypes.NewMsgAcknowledgement(packet, ack, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgAcknowledgement) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + balance, err := chainA.GetBalance(ctx, userBWallet.FormattedAddress(), chainADenom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance) + }) +} + +func (s *LocalhostInterchainAccountsTestSuite) TestInterchainAccounts_ReopenChannel_Localhost() { + t := s.T() + ctx := context.TODO() + + // relayer and channel output is discarded, only a single chain is required + _, _ = s.SetupChainsRelayerAndChannel(ctx) + chainA, _ := s.GetChains() + + chainADenom := chainA.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userBWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + msgChanOpenInitRes channeltypes.MsgChannelOpenInitResponse + msgChanOpenTryRes channeltypes.MsgChannelOpenTryResponse + ack []byte + packet channeltypes.Packet + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks") + + version := icatypes.NewDefaultMetadataString(exported.LocalhostConnectionID, exported.LocalhostConnectionID) + controllerPortID, err := icatypes.NewControllerPortID(userAWallet.FormattedAddress()) + s.Require().NoError(err) + + t.Run("channel open init localhost - broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.GetChannelId(), + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.GetChannelId(), + msgChanOpenTryRes.GetChannelId(), msgChanOpenTryRes.GetVersion(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.GetChannelId(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account registration and deposit funds", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + walletAmount := ibc.WalletAmount{ + Address: interchainAccAddress, + Amount: testvalues.StartingTokenAmount, + Denom: chainADenom, + } + + s.Require().NoError(chainA.SendFunds(ctx, interchaintest.FaucetAccountKeyName, walletAmount)) + }) + + t.Run("send localhost interchain accounts packet with timeout", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(1), packetData) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertValidTxResponse(txResp) + s.Require().NoError(err) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + packet, err = ibctesting.ParsePacketFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("timeout localhost interchain accounts packet", func(t *testing.T) { + msgTimeout := channeltypes.NewMsgTimeout(packet, 1, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgTimeout) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("close interchain accounts host channel end", func(t *testing.T) { + msgCloseConfirm := channeltypes.NewMsgChannelCloseConfirm(icatypes.HostPortID, msgChanOpenTryRes.ChannelId, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgCloseConfirm) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("verify localhost interchain accounts channel is closed", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.CLOSED, channelEndA.State, "the channel was not in an expected state") + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.ChannelId) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.CLOSED, channelEndB.State, "the channel was not in an expected state") + }) + + t.Run("channel open init localhost: create new channel for existing account", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + // note: response values are updated here in msgChanOpenInitRes + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.GetChannelId(), + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + // note: response values are updated here in msgChanOpenTryRes + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.GetChannelId(), + msgChanOpenTryRes.GetChannelId(), msgChanOpenTryRes.GetVersion(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.GetChannelId(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account and existing balance", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + balance, err := chainA.GetBalance(ctx, interchainAccAddress, chainADenom) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, balance) + }) + + t.Run("send packet localhost interchain accounts", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + txResp, err := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertValidTxResponse(txResp) + s.Require().NoError(err) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + packet, err = ibctesting.ParsePacketFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("recv packet localhost interchain accounts", func(t *testing.T) { + msgRecvPacket := channeltypes.NewMsgRecvPacket(packet, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgRecvPacket) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + ack, err = ibctesting.ParseAckFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(ack) + }) + + t.Run("acknowledge packet localhost interchain accounts", func(t *testing.T) { + msgAcknowledgement := channeltypes.NewMsgAcknowledgement(packet, ack, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgAcknowledgement) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, controllerPortID, msgChanOpenInitRes.GetChannelId(), 1) + + balance, err := chainA.GetBalance(ctx, userBWallet.FormattedAddress(), chainADenom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance) + }) +} diff --git a/e2e/tests/transfer/localhost_test.go b/e2e/tests/transfer/localhost_test.go new file mode 100644 index 00000000000..af42a1c3488 --- /dev/null +++ b/e2e/tests/transfer/localhost_test.go @@ -0,0 +1,170 @@ +package transfer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" + ibctesting "github.com/cosmos/ibc-go/v7/testing" + test "github.com/strangelove-ventures/interchaintest/v7/testutil" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" +) + +func TestTransferLocalhostTestSuite(t *testing.T) { + suite.Run(t, new(LocalhostTransferTestSuite)) +} + +type LocalhostTransferTestSuite struct { + testsuite.E2ETestSuite +} + +// TestMsgTransfer_Localhost creates two wallets on a single chain and performs MsgTransfers back and forth +// to ensure ibc functions as expected on localhost. This test is largely the same as TestMsgTransfer_Succeeds_Nonincentivized +// except that chain B is replaced with an additional wallet on chainA. +func (s *LocalhostTransferTestSuite) TestMsgTransfer_Localhost() { + t := s.T() + ctx := context.TODO() + + _, _ = s.SetupChainsRelayerAndChannel(ctx, transferChannelOptions()) + chainA, _ := s.GetChains() + + chainADenom := chainA.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userBWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + msgChanOpenInitRes channeltypes.MsgChannelOpenInitResponse + msgChanOpenTryRes channeltypes.MsgChannelOpenTryResponse + ack []byte + packet channeltypes.Packet + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks") + + t.Run("channel open init localhost", func(t *testing.T) { + msgChanOpenInit := channeltypes.NewMsgChannelOpenInit( + transfertypes.PortID, transfertypes.Version, + channeltypes.UNORDERED, []string{exported.LocalhostConnectionID}, + transfertypes.PortID, rlyWallet.FormattedAddress(), + ) + + s.Require().NoError(msgChanOpenInit.ValidateBasic()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenInit) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + transfertypes.PortID, transfertypes.Version, + channeltypes.UNORDERED, []string{exported.LocalhostConnectionID}, + transfertypes.PortID, msgChanOpenInitRes.GetChannelId(), + transfertypes.Version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + transfertypes.PortID, msgChanOpenInitRes.GetChannelId(), + msgChanOpenTryRes.GetChannelId(), transfertypes.Version, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + transfertypes.PortID, msgChanOpenTryRes.GetChannelId(), + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("query localhost transfer channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, transfertypes.PortID, msgChanOpenInitRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, transfertypes.PortID, msgChanOpenTryRes.GetChannelId()) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("send packet localhost ibc transfer", func(t *testing.T) { + txResp, err := s.Transfer(ctx, chainA, userAWallet, transfertypes.PortID, msgChanOpenInitRes.GetChannelId(), testvalues.DefaultTransferAmount(chainADenom), userAWallet.FormattedAddress(), userBWallet.FormattedAddress(), clienttypes.NewHeight(1, 100), 0, "") + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + packet, err = ibctesting.ParsePacketFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, userAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("recv packet localhost ibc transfer", func(t *testing.T) { + msgRecvPacket := channeltypes.NewMsgRecvPacket(packet, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgRecvPacket) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + events := testsuite.ABCIToSDKEvents(txResp.Events) + ack, err = ibctesting.ParseAckFromEvents(events) + s.Require().NoError(err) + s.Require().NotNil(ack) + }) + + t.Run("acknowledge packet localhost ibc transfer", func(t *testing.T) { + msgAcknowledgement := channeltypes.NewMsgAcknowledgement(packet, ack, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgAcknowledgement) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, transfertypes.PortID, msgChanOpenInitRes.GetChannelId(), 1) + + ibcToken := testsuite.GetIBCToken(chainADenom, transfertypes.PortID, msgChanOpenTryRes.GetChannelId()) + actualBalance, err := chainA.GetBalance(ctx, userBWallet.FormattedAddress(), ibcToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) +} diff --git a/e2e/tests/upgrades/upgrade_test.go b/e2e/tests/upgrades/upgrade_test.go index c8500e830dc..b4b46d8cb4c 100644 --- a/e2e/tests/upgrades/upgrade_test.go +++ b/e2e/tests/upgrades/upgrade_test.go @@ -25,6 +25,7 @@ import ( icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" v7migrations "github.com/cosmos/ibc-go/v7/modules/core/02-client/migrations/v7" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" "github.com/cosmos/ibc-go/v7/modules/core/exported" solomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" ibctesting "github.com/cosmos/ibc-go/v7/testing" @@ -604,6 +605,35 @@ func (s *UpgradeTestSuite) TestV6ToV7ChainUpgrade() { }) } +func (s *UpgradeTestSuite) TestV7ChainUpgradeAddLocalhost() { + t := s.T() + testCfg := testconfig.FromEnv() + + ctx := context.Background() + _, _ = s.SetupChainsRelayerAndChannel(ctx) + chain, _ := s.GetChains() + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chain), "failed to wait for blocks") + + t.Run("upgrade chain", func(t *testing.T) { + govProposalWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + s.UpgradeChain(ctx, chain, govProposalWallet, testCfg.UpgradePlanName, testCfg.ChainAConfig.Tag, testCfg.UpgradeTag) + }) + + t.Run("ensure the localhost client is active and sentinel connection is stored in state", func(t *testing.T) { + status, err := s.QueryClientStatus(ctx, chain, exported.LocalhostClientID) + s.Require().NoError(err) + s.Require().Equal(exported.Active.String(), status) + + connectionEnd, err := s.QueryConnection(ctx, chain, exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().Equal(connectiontypes.OPEN, connectionEnd.State) + s.Require().Equal(exported.LocalhostClientID, connectionEnd.ClientId) + s.Require().Equal(exported.LocalhostClientID, connectionEnd.Counterparty.ClientId) + s.Require().Equal(exported.LocalhostConnectionID, connectionEnd.Counterparty.ConnectionId) + }) +} + // RegisterInterchainAccount will attempt to register an interchain account on the counterparty chain. func (s *UpgradeTestSuite) RegisterInterchainAccount(ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, msgRegisterAccount *intertxtypes.MsgRegisterAccount) error { txResp, err := s.BroadcastMessages(ctx, chain, user, msgRegisterAccount) diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go index 5ad4a78febe..267f08886a9 100644 --- a/e2e/testsuite/codec.go +++ b/e2e/testsuite/codec.go @@ -1,8 +1,12 @@ package testsuite import ( + "encoding/hex" + "fmt" + "github.com/cosmos/cosmos-sdk/codec" sdkcodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/authz" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -25,11 +29,13 @@ import ( simappparams "github.com/cosmos/ibc-go/v7/testing/simapp/params" ) +// Codec returns the global E2E protobuf codec. func Codec() *codec.ProtoCodec { cdc, _ := codecAndEncodingConfig() return cdc } +// EncodingConfig returns the global E2E encoding config. func EncodingConfig() simappparams.EncodingConfig { _, cfg := codecAndEncodingConfig() return cfg @@ -64,3 +70,29 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, simappparams.EncodingConfig) { cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) return cdc, cfg } + +// UnmarshalMsgResponses attempts to unmarshal the tx msg responses into the provided message types. +func UnmarshalMsgResponses(txResp sdk.TxResponse, msgs ...codec.ProtoMarshaler) error { + cdc := Codec() + bz, err := hex.DecodeString(txResp.Data) + if err != nil { + return err + } + + var txMsgData sdk.TxMsgData + if err := cdc.Unmarshal(bz, &txMsgData); err != nil { + return err + } + + if len(msgs) != len(txMsgData.MsgResponses) { + return fmt.Errorf("expected %d message responses but got %d", len(msgs), len(txMsgData.MsgResponses)) + } + + for i, msg := range msgs { + if err := cdc.Unmarshal(txMsgData.MsgResponses[i].Value, msg); err != nil { + return err + } + } + + return nil +} diff --git a/e2e/testsuite/events.go b/e2e/testsuite/events.go new file mode 100644 index 00000000000..49a76aebe26 --- /dev/null +++ b/e2e/testsuite/events.go @@ -0,0 +1,21 @@ +package testsuite + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/cometbft/cometbft/abci/types" +) + +// ABCIToSDKEvents converts a list of ABCI events to Cosmos SDK events. +func ABCIToSDKEvents(abciEvents []abci.Event) sdk.Events { + var events sdk.Events + for _, evt := range abciEvents { + var attributes []sdk.Attribute + for _, attr := range evt.GetAttributes() { + attributes = append(attributes, sdk.NewAttribute(attr.Key, attr.Value)) + } + + events = events.AppendEvent(sdk.NewEvent(evt.GetType(), attributes...)) + } + + return events +} diff --git a/e2e/testsuite/grpc_query.go b/e2e/testsuite/grpc_query.go index ff38ae0f09e..a7449de94ec 100644 --- a/e2e/testsuite/grpc_query.go +++ b/e2e/testsuite/grpc_query.go @@ -12,6 +12,7 @@ import ( controllertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" ) @@ -47,6 +48,19 @@ func (s *E2ETestSuite) QueryClientStatus(ctx context.Context, chain ibc.Chain, c return res.Status, nil } +// QueryConnection queries the connection end using the given chain and connection id. +func (s *E2ETestSuite) QueryConnection(ctx context.Context, chain ibc.Chain, connectionID string) (connectiontypes.ConnectionEnd, error) { + queryClient := s.GetChainGRCPClients(chain).ConnectionQueryClient + res, err := queryClient.Connection(ctx, &connectiontypes.QueryConnectionRequest{ + ConnectionId: connectionID, + }) + if err != nil { + return connectiontypes.ConnectionEnd{}, err + } + + return *res.Connection, nil +} + // QueryChannel queries the channel on a given chain for the provided portID and channelID func (s *E2ETestSuite) QueryChannel(ctx context.Context, chain ibc.Chain, portID, channelID string) (channeltypes.Channel, error) { queryClient := s.GetChainGRCPClients(chain).ChannelQueryClient diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index 965e4177c86..2b3de38b21b 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -2,7 +2,6 @@ package testsuite import ( "context" - "errors" "fmt" "strconv" "strings" @@ -41,6 +40,7 @@ import ( feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ) @@ -72,11 +72,12 @@ type E2ETestSuite struct { // These should typically be used for query clients only. If we need to make changes, we should // use E2ETestSuite.BroadcastMessages to broadcast transactions instead. type GRPCClients struct { - ClientQueryClient clienttypes.QueryClient - ChannelQueryClient channeltypes.QueryClient - FeeQueryClient feetypes.QueryClient - ICAQueryClient controllertypes.QueryClient - InterTxQueryClient intertxtypes.QueryClient + ClientQueryClient clienttypes.QueryClient + ConnectionQueryClient connectiontypes.QueryClient + ChannelQueryClient channeltypes.QueryClient + FeeQueryClient feetypes.QueryClient + ICAQueryClient controllertypes.QueryClient + InterTxQueryClient intertxtypes.QueryClient // SDK query clients GovQueryClient govtypesv1beta1.QueryClient @@ -407,17 +408,18 @@ func (s *E2ETestSuite) InitGRPCClients(chain *cosmos.CosmosChain) { } s.grpcClients[chain.Config().ChainID] = GRPCClients{ - ClientQueryClient: clienttypes.NewQueryClient(grpcConn), - ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), - FeeQueryClient: feetypes.NewQueryClient(grpcConn), - ICAQueryClient: controllertypes.NewQueryClient(grpcConn), - InterTxQueryClient: intertxtypes.NewQueryClient(grpcConn), - GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), - GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), - GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), - ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), - AuthQueryClient: authtypes.NewQueryClient(grpcConn), - AuthZQueryClient: authz.NewQueryClient(grpcConn), + ClientQueryClient: clienttypes.NewQueryClient(grpcConn), + ConnectionQueryClient: connectiontypes.NewQueryClient(grpcConn), + ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), + FeeQueryClient: feetypes.NewQueryClient(grpcConn), + ICAQueryClient: controllertypes.NewQueryClient(grpcConn), + InterTxQueryClient: intertxtypes.NewQueryClient(grpcConn), + GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), + GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), + GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), + ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), + AuthQueryClient: authtypes.NewQueryClient(grpcConn), + AuthZQueryClient: authz.NewQueryClient(grpcConn), } } @@ -577,7 +579,7 @@ func (s *E2ETestSuite) QueryModuleAccountAddress(ctx context.Context, moduleName } moduleAccount, ok := account.(authtypes.ModuleAccountI) if !ok { - return nil, errors.New(fmt.Sprintf("failed to cast account: %T as ModuleAccount", moduleAccount)) + return nil, fmt.Errorf("failed to cast account: %T as ModuleAccount", moduleAccount) } return moduleAccount.GetAddress(), nil diff --git a/modules/core/02-client/abci.go b/modules/core/02-client/abci.go index 735834b2f65..d5b4b2deb65 100644 --- a/modules/core/02-client/abci.go +++ b/modules/core/02-client/abci.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ibc-go/v7/modules/core/02-client/keeper" + "github.com/cosmos/ibc-go/v7/modules/core/exported" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" ) @@ -31,4 +32,11 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { keeper.EmitUpgradeChainEvent(ctx, plan.Height) } } + + // update the localhost client with the latest block height if it is active. + if clientState, found := k.GetClientState(ctx, exported.Localhost); found { + if k.GetClientStatus(ctx, clientState, exported.Localhost) == exported.Active { + k.UpdateLocalhostClient(ctx, clientState) + } + } } diff --git a/modules/core/02-client/genesis.go b/modules/core/02-client/genesis.go index aae41d9ae5a..7a38683ea03 100644 --- a/modules/core/02-client/genesis.go +++ b/modules/core/02-client/genesis.go @@ -46,6 +46,12 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, gs types.GenesisState) { } k.SetNextClientSequence(ctx, gs.NextClientSequence) + + // if the localhost already exists in state (included in the genesis file), + // it must be overwritten to ensure its stored height equals the context block height + if err := k.CreateLocalhostClient(ctx); err != nil { + panic(fmt.Sprintf("failed to initialise localhost client: %s", err.Error())) + } } // ExportGenesis returns the ibc client submodule's exported genesis. diff --git a/modules/core/02-client/keeper/client.go b/modules/core/02-client/keeper/client.go index 8cb5a02b2d2..4935cc13299 100644 --- a/modules/core/02-client/keeper/client.go +++ b/modules/core/02-client/keeper/client.go @@ -16,6 +16,10 @@ import ( func (k Keeper) CreateClient( ctx sdk.Context, clientState exported.ClientState, consensusState exported.ConsensusState, ) (string, error) { + if clientState.ClientType() == exported.Localhost { + return "", errorsmod.Wrapf(types.ErrInvalidClientType, "cannot create client of type: %s", clientState.ClientType()) + } + params := k.GetParams(ctx) if !params.IsAllowedClient(clientState.ClientType()) { return "", errorsmod.Wrapf( @@ -53,7 +57,7 @@ func (k Keeper) UpdateClient(ctx sdk.Context, clientID string, clientMsg exporte clientStore := k.ClientStore(ctx, clientID) - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(types.ErrClientNotActive, "cannot update client (%s) with status %s", clientID, status) } @@ -114,7 +118,7 @@ func (k Keeper) UpgradeClient(ctx sdk.Context, clientID string, upgradedClient e clientStore := k.ClientStore(ctx, clientID) - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(types.ErrClientNotActive, "cannot upgrade client (%s) with status %s", clientID, status) } diff --git a/modules/core/02-client/keeper/client_test.go b/modules/core/02-client/keeper/client_test.go index 94b5fed855a..ce338d25853 100644 --- a/modules/core/02-client/keeper/client_test.go +++ b/modules/core/02-client/keeper/client_test.go @@ -13,22 +13,39 @@ import ( "github.com/cosmos/ibc-go/v7/modules/core/exported" solomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" ibctesting "github.com/cosmos/ibc-go/v7/testing" ) func (suite *KeeperTestSuite) TestCreateClient() { cases := []struct { - msg string - clientState exported.ClientState - expPass bool + msg string + clientState exported.ClientState + consensusState exported.ConsensusState + expPass bool }{ - {"success", ibctm.NewClientState(testChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), true}, - {"client type not supported", solomachine.NewClientState(0, &solomachine.ConsensusState{PublicKey: suite.solomachine.ConsensusState().PublicKey, Diversifier: suite.solomachine.Diversifier, Timestamp: suite.solomachine.Time}), false}, + { + "success: 07-tendermint client type supported", + ibctm.NewClientState(testChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), + suite.consensusState, + true, + }, + { + "success: 06-solomachine client type supported", + solomachine.NewClientState(0, &solomachine.ConsensusState{PublicKey: suite.solomachine.ConsensusState().PublicKey, Diversifier: suite.solomachine.Diversifier, Timestamp: suite.solomachine.Time}), + &solomachine.ConsensusState{PublicKey: suite.solomachine.ConsensusState().PublicKey, Diversifier: suite.solomachine.Diversifier, Timestamp: suite.solomachine.Time}, + true, + }, + { + "failure: 09-localhost client type not supported", + localhost.NewClientState(clienttypes.GetSelfHeight(suite.ctx)), + nil, + false, + }, } for i, tc := range cases { - - clientID, err := suite.keeper.CreateClient(suite.ctx, tc.clientState, suite.consensusState) + clientID, err := suite.keeper.CreateClient(suite.ctx, tc.clientState, tc.consensusState) if tc.expPass { suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.msg) suite.Require().NotNil(clientID, "valid test case %d failed: %s", i, tc.msg) diff --git a/modules/core/02-client/keeper/grpc_query.go b/modules/core/02-client/keeper/grpc_query.go index 5f575303c02..073f59b2995 100644 --- a/modules/core/02-client/keeper/grpc_query.go +++ b/modules/core/02-client/keeper/grpc_query.go @@ -245,8 +245,7 @@ func (q Keeper) ClientStatus(c context.Context, req *types.QueryClientStatusRequ ) } - clientStore := q.ClientStore(ctx, req.ClientId) - status := clientState.Status(ctx, clientStore, q.cdc) + status := q.GetClientStatus(ctx, clientState, req.ClientId) return &types.QueryClientStatusResponse{ Status: status.String(), diff --git a/modules/core/02-client/keeper/grpc_query_test.go b/modules/core/02-client/keeper/grpc_query_test.go index 50683e98723..6b48f466aba 100644 --- a/modules/core/02-client/keeper/grpc_query_test.go +++ b/modules/core/02-client/keeper/grpc_query_test.go @@ -111,13 +111,17 @@ func (suite *KeeperTestSuite) TestQueryClientStates() { { "empty pagination", func() { + localhost := types.NewIdentifiedClientState(exported.LocalhostClientID, suite.chainA.GetClientState(exported.LocalhostClientID)) + expClientStates = types.IdentifiedClientStates{localhost} req = &types.QueryClientStatesRequest{} }, true, }, { - "success, no results", + "success, only localhost", func() { + localhost := types.NewIdentifiedClientState(exported.LocalhostClientID, suite.chainA.GetClientState(exported.LocalhostClientID)) + expClientStates = types.IdentifiedClientStates{localhost} req = &types.QueryClientStatesRequest{ Pagination: &query.PageRequest{ Limit: 3, @@ -139,11 +143,12 @@ func (suite *KeeperTestSuite) TestQueryClientStates() { clientStateA1 := path1.EndpointA.GetClientState() clientStateA2 := path2.EndpointA.GetClientState() + localhost := types.NewIdentifiedClientState(exported.LocalhostClientID, suite.chainA.GetClientState(exported.LocalhostClientID)) idcs := types.NewIdentifiedClientState(path1.EndpointA.ClientID, clientStateA1) idcs2 := types.NewIdentifiedClientState(path2.EndpointA.ClientID, clientStateA2) // order is sorted by client id - expClientStates = types.IdentifiedClientStates{idcs, idcs2}.Sort() + expClientStates = types.IdentifiedClientStates{localhost, idcs, idcs2}.Sort() req = &types.QueryClientStatesRequest{ Pagination: &query.PageRequest{ Limit: 20, @@ -158,10 +163,10 @@ func (suite *KeeperTestSuite) TestQueryClientStates() { for _, tc := range testCases { suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { suite.SetupTest() // reset + tc.malleate() ctx := sdk.WrapSDKContext(suite.chainA.GetContext()) - res, err := suite.chainA.QueryServer.ClientStates(ctx, req) if tc.expPass { suite.Require().NoError(err) diff --git a/modules/core/02-client/keeper/keeper.go b/modules/core/02-client/keeper/keeper.go index e7b34b39226..2d777546f81 100644 --- a/modules/core/02-client/keeper/keeper.go +++ b/modules/core/02-client/keeper/keeper.go @@ -21,6 +21,7 @@ import ( host "github.com/cosmos/ibc-go/v7/modules/core/24-host" "github.com/cosmos/ibc-go/v7/modules/core/exported" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" ) // Keeper represents a type that grants read and write permissions to any client @@ -54,6 +55,17 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+exported.ModuleName+"/"+types.SubModuleName) } +// CreateLocalhostClient initialises the 09-localhost client state and sets it in state. +func (k Keeper) CreateLocalhostClient(ctx sdk.Context) error { + var clientState localhost.ClientState + return clientState.Initialize(ctx, k.cdc, k.ClientStore(ctx, exported.LocalhostClientID), nil) +} + +// UpdateLocalhostClient updates the 09-localhost client to the latest block height and chain ID. +func (k Keeper) UpdateLocalhostClient(ctx sdk.Context, clientState exported.ClientState) []exported.Height { + return clientState.UpdateState(ctx, k.cdc, k.ClientStore(ctx, exported.LocalhostClientID), nil) +} + // GenerateClientIdentifier returns the next client identifier. func (k Keeper) GenerateClientIdentifier(ctx sdk.Context, clientType string) string { nextClientSeq := k.GetNextClientSequence(ctx) @@ -392,3 +404,12 @@ func (k Keeper) ClientStore(ctx sdk.Context, clientID string) sdk.KVStore { clientPrefix := []byte(fmt.Sprintf("%s/%s/", host.KeyClientStorePrefix, clientID)) return prefix.NewStore(ctx.KVStore(k.storeKey), clientPrefix) } + +// GetClientStatus returns the status for a given clientState. If the client type is not in the allowed +// clients param field, Unauthorized is returned, otherwise the client state status is returned. +func (k Keeper) GetClientStatus(ctx sdk.Context, clientState exported.ClientState, clientID string) exported.Status { + if !k.GetParams(ctx).IsAllowedClient(clientState.ClientType()) { + return exported.Unauthorized + } + return clientState.Status(ctx, k.ClientStore(ctx, clientID), k.cdc) +} diff --git a/modules/core/02-client/keeper/keeper_test.go b/modules/core/02-client/keeper/keeper_test.go index dc59b78bf9d..839ee00553d 100644 --- a/modules/core/02-client/keeper/keeper_test.go +++ b/modules/core/02-client/keeper/keeper_test.go @@ -21,6 +21,7 @@ import ( "github.com/cosmos/ibc-go/v7/modules/core/exported" solomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" ibctesting "github.com/cosmos/ibc-go/v7/testing" ibctestingmock "github.com/cosmos/ibc-go/v7/testing/mock" "github.com/cosmos/ibc-go/v7/testing/simapp" @@ -236,9 +237,10 @@ func (suite *KeeperTestSuite) TestValidateSelfClient() { func (suite KeeperTestSuite) TestGetAllGenesisClients() { //nolint:govet // this is a test, we are okay with copying locks clientIDs := []string{ - testClientID2, testClientID3, testClientID, + exported.LocalhostClientID, testClientID2, testClientID3, testClientID, } expClients := []exported.ClientState{ + localhost.NewClientState(types.GetSelfHeight(suite.chainA.GetContext())), ibctm.NewClientState(testChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, types.ZeroHeight(), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), ibctm.NewClientState(testChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, types.ZeroHeight(), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), ibctm.NewClientState(testChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, types.ZeroHeight(), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), @@ -418,22 +420,31 @@ func (suite KeeperTestSuite) TestIterateClientStates() { //nolint:govet // this testCases := []struct { name string prefix []byte - expClientIDs []string + expClientIDs func() []string }{ { "all clientIDs", nil, - append(expSMClientIDs, expTMClientIDs...), + func() []string { + allClientIDs := []string{exported.LocalhostClientID} + allClientIDs = append(allClientIDs, expSMClientIDs...) + allClientIDs = append(allClientIDs, expTMClientIDs...) + return allClientIDs + }, }, { "tendermint clientIDs", []byte(exported.Tendermint), - expTMClientIDs, + func() []string { + return expTMClientIDs + }, }, { "solo machine clientIDs", []byte(exported.Solomachine), - expSMClientIDs, + func() []string { + return expSMClientIDs + }, }, } @@ -446,7 +457,7 @@ func (suite KeeperTestSuite) TestIterateClientStates() { //nolint:govet // this return false }) - suite.Require().Equal(tc.expClientIDs, clientIDs) + suite.Require().ElementsMatch(tc.expClientIDs(), clientIDs) }) } } diff --git a/modules/core/02-client/keeper/migrations.go b/modules/core/02-client/keeper/migrations.go index ad3587ae901..b8290133042 100644 --- a/modules/core/02-client/keeper/migrations.go +++ b/modules/core/02-client/keeper/migrations.go @@ -16,7 +16,7 @@ func NewMigrator(keeper Keeper) Migrator { return Migrator{keeper: keeper} } -// Migrate2to3 migrates from version 2 to 3. +// Migrate2to3 migrates from consensus version 2 to 3. // This migration // - migrates solo machine client states from v2 to v3 protobuf definition // - prunes solo machine consensus states @@ -25,3 +25,9 @@ func NewMigrator(keeper Keeper) Migrator { func (m Migrator) Migrate2to3(ctx sdk.Context) error { return v7.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc, m.keeper) } + +// Migrate3to4 migrates from consensus version 3 to 4. +// This migration enables the localhost client. +func (m Migrator) Migrate3to4(ctx sdk.Context) error { + return v7.MigrateLocalhostClient(ctx, m.keeper) +} diff --git a/modules/core/02-client/keeper/proposal.go b/modules/core/02-client/keeper/proposal.go index a56d9b6ca72..1dc7edd33e0 100644 --- a/modules/core/02-client/keeper/proposal.go +++ b/modules/core/02-client/keeper/proposal.go @@ -25,7 +25,7 @@ func (k Keeper) ClientUpdateProposal(ctx sdk.Context, p *types.ClientUpdatePropo subjectClientStore := k.ClientStore(ctx, p.SubjectClientId) - if status := subjectClientState.Status(ctx, subjectClientStore, k.cdc); status == exported.Active { + if status := k.GetClientStatus(ctx, subjectClientState, p.SubjectClientId); status == exported.Active { return errorsmod.Wrap(types.ErrInvalidUpdateClientProposal, "cannot update Active subject client") } @@ -40,7 +40,7 @@ func (k Keeper) ClientUpdateProposal(ctx sdk.Context, p *types.ClientUpdatePropo substituteClientStore := k.ClientStore(ctx, p.SubstituteClientId) - if status := substituteClientState.Status(ctx, substituteClientStore, k.cdc); status != exported.Active { + if status := k.GetClientStatus(ctx, substituteClientState, p.SubstituteClientId); status != exported.Active { return errorsmod.Wrapf(types.ErrClientNotActive, "substitute client is not Active, status is %s", status) } diff --git a/modules/core/02-client/migrations/v7/expected_keepers.go b/modules/core/02-client/migrations/v7/expected_keepers.go index 6d424cc0370..f36be5fabe2 100644 --- a/modules/core/02-client/migrations/v7/expected_keepers.go +++ b/modules/core/02-client/migrations/v7/expected_keepers.go @@ -11,4 +11,5 @@ type ClientKeeper interface { GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) SetClientState(ctx sdk.Context, clientID string, clientState exported.ClientState) ClientStore(ctx sdk.Context, clientID string) sdk.KVStore + CreateLocalhostClient(ctx sdk.Context) error } diff --git a/modules/core/02-client/migrations/v7/localhost.go b/modules/core/02-client/migrations/v7/localhost.go new file mode 100644 index 00000000000..49709b9db9f --- /dev/null +++ b/modules/core/02-client/migrations/v7/localhost.go @@ -0,0 +1,10 @@ +package v7 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MigrateLocalhostClient initialises the 09-localhost client state and sets it in state. +func MigrateLocalhostClient(ctx sdk.Context, clientKeeper ClientKeeper) error { + return clientKeeper.CreateLocalhostClient(ctx) +} diff --git a/modules/core/02-client/migrations/v7/localhost_test.go b/modules/core/02-client/migrations/v7/localhost_test.go new file mode 100644 index 00000000000..fc09523a94c --- /dev/null +++ b/modules/core/02-client/migrations/v7/localhost_test.go @@ -0,0 +1,26 @@ +package v7_test + +import ( + v7 "github.com/cosmos/ibc-go/v7/modules/core/02-client/migrations/v7" + host "github.com/cosmos/ibc-go/v7/modules/core/24-host" + "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +func (suite *MigrationsV7TestSuite) TestMigrateLocalhostClient() { + suite.SetupTest() + + // note: explicitly remove the localhost client before running migration handler + clientStore := suite.chainA.GetSimApp().GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), exported.LocalhostClientID) + clientStore.Delete(host.ClientStateKey()) + + clientState, found := suite.chainA.GetSimApp().GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), exported.LocalhostClientID) + suite.Require().False(found) + suite.Require().Nil(clientState) + + err := v7.MigrateLocalhostClient(suite.chainA.GetContext(), suite.chainA.GetSimApp().GetIBCKeeper().ClientKeeper) + suite.Require().NoError(err) + + clientState, found = suite.chainA.GetSimApp().GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), exported.LocalhostClientID) + suite.Require().True(found) + suite.Require().NotNil(clientState) +} diff --git a/modules/core/02-client/types/client.pb.go b/modules/core/02-client/types/client.pb.go index 6b4f4351245..badaa482f47 100644 --- a/modules/core/02-client/types/client.pb.go +++ b/modules/core/02-client/types/client.pb.go @@ -341,7 +341,9 @@ var xxx_messageInfo_Height proto.InternalMessageInfo // Params defines the set of IBC light client parameters. type Params struct { - // allowed_clients defines the list of allowed client state types. + // allowed_clients defines the list of allowed client state types which can be created + // and interacted with. If a client type is removed from the allowed clients list, usage + // of this client will be disabled until it is added again to the list. AllowedClients []string `protobuf:"bytes,1,rep,name=allowed_clients,json=allowedClients,proto3" json:"allowed_clients,omitempty" yaml:"allowed_clients"` } diff --git a/modules/core/02-client/types/errors.go b/modules/core/02-client/types/errors.go index a0fa91e8dac..cdc84871ca0 100644 --- a/modules/core/02-client/types/errors.go +++ b/modules/core/02-client/types/errors.go @@ -34,4 +34,6 @@ var ( ErrInvalidSubstitute = errorsmod.Register(SubModuleName, 27, "invalid client state substitute") ErrInvalidUpgradeProposal = errorsmod.Register(SubModuleName, 28, "invalid upgrade proposal") ErrClientNotActive = errorsmod.Register(SubModuleName, 29, "client state is not active") + ErrFailedMembershipVerification = errorsmod.Register(SubModuleName, 30, "membership verification failed") + ErrFailedNonMembershipVerification = errorsmod.Register(SubModuleName, 31, "non-membership verification failed") ) diff --git a/modules/core/02-client/types/keys.go b/modules/core/02-client/types/keys.go index 12345c46e6c..892268e2368 100644 --- a/modules/core/02-client/types/keys.go +++ b/modules/core/02-client/types/keys.go @@ -9,6 +9,7 @@ import ( errorsmod "cosmossdk.io/errors" host "github.com/cosmos/ibc-go/v7/modules/core/24-host" + "github.com/cosmos/ibc-go/v7/modules/core/exported" ) const ( @@ -49,6 +50,10 @@ func IsValidClientID(clientID string) bool { // ParseClientIdentifier parses the client type and sequence from the client identifier. func ParseClientIdentifier(clientID string) (string, uint64, error) { + if clientID == exported.LocalhostClientID { + return clientID, 0, nil + } + if !IsClientIDFormat(clientID) { return "", 0, errorsmod.Wrapf(host.ErrInvalidID, "invalid client identifier %s is not in format: `{client-type}-{N}`", clientID) } diff --git a/modules/core/02-client/types/params.go b/modules/core/02-client/types/params.go index 21ad31007df..b0c43b156ef 100644 --- a/modules/core/02-client/types/params.go +++ b/modules/core/02-client/types/params.go @@ -10,8 +10,8 @@ import ( ) var ( - // DefaultAllowedClients are "06-solomachine" and "07-tendermint" - DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint} + // DefaultAllowedClients are the default clients for the AllowedClients parameter. + DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint, exported.Localhost} // KeyAllowedClients is store's key for AllowedClients Params KeyAllowedClients = []byte("AllowedClients") @@ -29,7 +29,7 @@ func NewParams(allowedClients ...string) Params { } } -// DefaultParams is the default parameter configuration for the ibc-client module +// DefaultParams is the default parameter configuration for the ibc-client module. func DefaultParams() Params { return NewParams(DefaultAllowedClients...) } diff --git a/modules/core/03-connection/genesis.go b/modules/core/03-connection/genesis.go index 44a8585bcdb..df8fb28945e 100644 --- a/modules/core/03-connection/genesis.go +++ b/modules/core/03-connection/genesis.go @@ -19,6 +19,8 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, gs types.GenesisState) { } k.SetNextConnectionSequence(ctx, gs.NextConnectionSequence) k.SetParams(ctx, gs.Params) + + k.CreateSentinelLocalhostConnection(ctx) } // ExportGenesis returns the ibc connection submodule's exported genesis. diff --git a/modules/core/03-connection/keeper/grpc_query_test.go b/modules/core/03-connection/keeper/grpc_query_test.go index dd9531fb43e..0573e3e02aa 100644 --- a/modules/core/03-connection/keeper/grpc_query_test.go +++ b/modules/core/03-connection/keeper/grpc_query_test.go @@ -87,9 +87,15 @@ func (suite *KeeperTestSuite) TestQueryConnection() { } func (suite *KeeperTestSuite) TestQueryConnections() { + suite.chainA.App.GetIBCKeeper().ConnectionKeeper.CreateSentinelLocalhostConnection(suite.chainA.GetContext()) + localhostConn, found := suite.chainA.App.GetIBCKeeper().ConnectionKeeper.GetConnection(suite.chainA.GetContext(), exported.LocalhostConnectionID) + suite.Require().True(found) + + identifiedConn := types.NewIdentifiedConnection(exported.LocalhostConnectionID, localhostConn) + var ( req *types.QueryConnectionsRequest - expConnections = []*types.IdentifiedConnection{} + expConnections = []*types.IdentifiedConnection{&identifiedConn} ) testCases := []struct { @@ -137,11 +143,11 @@ func (suite *KeeperTestSuite) TestQueryConnections() { iconn2 := types.NewIdentifiedConnection(path2.EndpointA.ConnectionID, conn2) iconn3 := types.NewIdentifiedConnection(path3.EndpointA.ConnectionID, conn3) - expConnections = []*types.IdentifiedConnection{&iconn1, &iconn2, &iconn3} + expConnections = []*types.IdentifiedConnection{&iconn1, &iconn2, &iconn3, &identifiedConn} req = &types.QueryConnectionsRequest{ Pagination: &query.PageRequest{ - Limit: 3, + Limit: 4, CountTotal: true, }, } diff --git a/modules/core/03-connection/keeper/handshake.go b/modules/core/03-connection/keeper/handshake.go index 602547c7781..523296194d3 100644 --- a/modules/core/03-connection/keeper/handshake.go +++ b/modules/core/03-connection/keeper/handshake.go @@ -33,6 +33,15 @@ func (k Keeper) ConnOpenInit( versions = []exported.Version{version} } + clientState, found := k.clientKeeper.GetClientState(ctx, clientID) + if !found { + return "", errorsmod.Wrapf(clienttypes.ErrClientNotFound, "clientID (%s)", clientID) + } + + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { + return "", errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + } + connectionID := k.GenerateConnectionIdentifier(ctx) if err := k.addConnectionToClient(ctx, clientID, connectionID); err != nil { return "", err @@ -75,6 +84,8 @@ func (k Keeper) ConnOpenTry( // generate a new connection connectionID := k.GenerateConnectionIdentifier(ctx) + // check that the consensus height the counterparty chain is using to store a representation + // of this chain's consensus state is at a height in the past selfHeight := clienttypes.GetSelfHeight(ctx) if consensusHeight.GTE(selfHeight) { return "", errorsmod.Wrapf( @@ -164,7 +175,8 @@ func (k Keeper) ConnOpenAck( proofHeight exported.Height, // height that relayer constructed proofTry consensusHeight exported.Height, // latest height of chainA that chainB has stored on its chainA client ) error { - // Check that chainB client hasn't stored invalid height + // check that the consensus height the counterparty chain is using to store a representation + // of this chain's consensus state is at a height in the past selfHeight := clienttypes.GetSelfHeight(ctx) if consensusHeight.GTE(selfHeight) { return errorsmod.Wrapf( diff --git a/modules/core/03-connection/keeper/handshake_test.go b/modules/core/03-connection/keeper/handshake_test.go index c14ec879204..5751c9e531a 100644 --- a/modules/core/03-connection/keeper/handshake_test.go +++ b/modules/core/03-connection/keeper/handshake_test.go @@ -15,10 +15,11 @@ import ( // chainB which is yet UNINITIALIZED func (suite *KeeperTestSuite) TestConnOpenInit() { var ( - path *ibctesting.Path - version *types.Version - delayPeriod uint64 - emptyConnBID bool + path *ibctesting.Path + version *types.Version + delayPeriod uint64 + emptyConnBID bool + expErrorMsgSubstring string ) testCases := []struct { @@ -45,6 +46,17 @@ func (suite *KeeperTestSuite) TestConnOpenInit() { // set path.EndpointA.ClientID to invalid client identifier path.EndpointA.ClientID = "clientidentifier" }, false}, + { + msg: "unauthorized client", + expPass: false, + malleate: func() { + expErrorMsgSubstring = "status is Unauthorized" + // remove client from allowed list + params := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetParams(suite.chainA.GetContext()) + params.AllowedClients = []string{} + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetParams(suite.chainA.GetContext(), params) + }, + }, } for _, tc := range testCases { @@ -53,6 +65,7 @@ func (suite *KeeperTestSuite) TestConnOpenInit() { suite.SetupTest() // reset emptyConnBID = false // must be explicitly changed version = nil // must be explicitly changed + expErrorMsgSubstring = "" path = ibctesting.NewPath(suite.chainA, suite.chainB) suite.coordinator.SetupClients(path) @@ -70,6 +83,7 @@ func (suite *KeeperTestSuite) TestConnOpenInit() { suite.Require().Equal(types.FormatConnectionIdentifier(0), connectionID) } else { suite.Require().Error(err) + suite.Contains(err.Error(), expErrorMsgSubstring) suite.Require().Equal("", connectionID) } }) diff --git a/modules/core/03-connection/keeper/keeper.go b/modules/core/03-connection/keeper/keeper.go index a0ab8e2a731..4e763c3302b 100644 --- a/modules/core/03-connection/keeper/keeper.go +++ b/modules/core/03-connection/keeper/keeper.go @@ -196,6 +196,14 @@ func (k Keeper) GetAllConnections(ctx sdk.Context) (connections []types.Identifi return connections } +// CreateSentinelLocalhostConnection creates and sets the sentinel localhost connection end in the IBC store. +func (k Keeper) CreateSentinelLocalhostConnection(ctx sdk.Context) { + counterparty := types.NewCounterparty(exported.LocalhostClientID, exported.LocalhostConnectionID, commitmenttypes.NewMerklePrefix(k.GetCommitmentPrefix().Bytes())) + connectionEnd := types.NewConnectionEnd(types.OPEN, exported.LocalhostClientID, counterparty, types.ExportedVersionsToProto(types.GetCompatibleVersions()), 0) + + k.SetConnection(ctx, exported.LocalhostConnectionID, connectionEnd) +} + // addConnectionToClient is used to add a connection identifier to the set of // connections associated with a client. func (k Keeper) addConnectionToClient(ctx sdk.Context, clientID, connectionID string) error { diff --git a/modules/core/03-connection/keeper/keeper_test.go b/modules/core/03-connection/keeper/keeper_test.go index a82573788d3..dbae331d690 100644 --- a/modules/core/03-connection/keeper/keeper_test.go +++ b/modules/core/03-connection/keeper/keeper_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" "github.com/cosmos/ibc-go/v7/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v7/testing" ) @@ -79,7 +80,11 @@ func (suite KeeperTestSuite) TestGetAllConnections() { //nolint:govet // this is iconn1 := types.NewIdentifiedConnection(path1.EndpointA.ConnectionID, conn1) iconn2 := types.NewIdentifiedConnection(path2.EndpointA.ConnectionID, conn2) - expConnections := []types.IdentifiedConnection{iconn1, iconn2} + suite.chainA.App.GetIBCKeeper().ConnectionKeeper.CreateSentinelLocalhostConnection(suite.chainA.GetContext()) + localhostConn, found := suite.chainA.App.GetIBCKeeper().ConnectionKeeper.GetConnection(suite.chainA.GetContext(), exported.LocalhostConnectionID) + suite.Require().True(found) + + expConnections := []types.IdentifiedConnection{iconn1, iconn2, types.NewIdentifiedConnection(exported.LocalhostConnectionID, localhostConn)} connections := suite.chainA.App.GetIBCKeeper().ConnectionKeeper.GetAllConnections(suite.chainA.GetContext()) suite.Require().Len(connections, len(expConnections)) @@ -156,3 +161,18 @@ func (suite *KeeperTestSuite) TestGetTimestampAtHeight() { }) } } + +func (suite *KeeperTestSuite) TestLocalhostConnectionEndCreation() { + ctx := suite.chainA.GetContext() + connectionKeeper := suite.chainA.App.GetIBCKeeper().ConnectionKeeper + connectionKeeper.CreateSentinelLocalhostConnection(ctx) + + connectionEnd, found := connectionKeeper.GetConnection(ctx, exported.LocalhostConnectionID) + + suite.Require().True(found) + suite.Require().Equal(types.OPEN, connectionEnd.State) + suite.Require().Equal(exported.LocalhostClientID, connectionEnd.ClientId) + suite.Require().Equal(types.ExportedVersionsToProto(types.GetCompatibleVersions()), connectionEnd.Versions) + expectedCounterParty := types.NewCounterparty(exported.LocalhostClientID, exported.LocalhostConnectionID, commitmenttypes.NewMerklePrefix(connectionKeeper.GetCommitmentPrefix().Bytes())) + suite.Require().Equal(expectedCounterParty, connectionEnd.Counterparty) +} diff --git a/modules/core/03-connection/keeper/migrations.go b/modules/core/03-connection/keeper/migrations.go new file mode 100644 index 00000000000..9965eab28ca --- /dev/null +++ b/modules/core/03-connection/keeper/migrations.go @@ -0,0 +1,24 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + connectionv7 "github.com/cosmos/ibc-go/v7/modules/core/03-connection/migrations/v7" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + keeper Keeper +} + +// NewMigrator returns a new Migrator. +func NewMigrator(keeper Keeper) Migrator { + return Migrator{keeper: keeper} +} + +// Migrate3to4 migrates from version 3 to 4. +// This migration writes the sentinel localhost connection end to state. +func (m Migrator) Migrate3to4(ctx sdk.Context) error { + connectionv7.MigrateLocalhostConnection(ctx, m.keeper) + return nil +} diff --git a/modules/core/03-connection/keeper/verify.go b/modules/core/03-connection/keeper/verify.go index 4300ae3fa64..22f23d70866 100644 --- a/modules/core/03-connection/keeper/verify.go +++ b/modules/core/03-connection/keeper/verify.go @@ -25,19 +25,17 @@ func (k Keeper) VerifyClientState( clientState exported.ClientState, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - targetClient, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + targetClient, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := targetClient.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, targetClient, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } merklePath := commitmenttypes.NewMerklePath(host.FullClientStatePath(connection.GetCounterparty().GetClientID())) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -69,19 +67,17 @@ func (k Keeper) VerifyClientConsensusState( consensusState exported.ConsensusState, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } merklePath := commitmenttypes.NewMerklePath(host.FullConsensusStatePath(connection.GetCounterparty().GetClientID(), consensusHeight)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -113,19 +109,17 @@ func (k Keeper) VerifyConnectionState( counterpartyConnection exported.ConnectionI, // opposite connection ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } merklePath := commitmenttypes.NewMerklePath(host.ConnectionPath(connectionID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -163,19 +157,17 @@ func (k Keeper) VerifyChannelState( channel exported.ChannelI, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } merklePath := commitmenttypes.NewMerklePath(host.ChannelPath(portID, channelID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -214,14 +206,12 @@ func (k Keeper) VerifyPacketCommitment( commitmentBytes []byte, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } @@ -230,7 +220,7 @@ func (k Keeper) VerifyPacketCommitment( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketCommitmentPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -259,14 +249,12 @@ func (k Keeper) VerifyPacketAcknowledgement( acknowledgement []byte, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } @@ -275,7 +263,7 @@ func (k Keeper) VerifyPacketAcknowledgement( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketAcknowledgementPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -304,14 +292,12 @@ func (k Keeper) VerifyPacketReceiptAbsence( sequence uint64, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } @@ -320,7 +306,7 @@ func (k Keeper) VerifyPacketReceiptAbsence( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketReceiptPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -348,14 +334,12 @@ func (k Keeper) VerifyNextSequenceRecv( nextSequenceRecv uint64, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, clientStore, err := k.getClientStateAndVerificationStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, clientID); status != exported.Active { return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } @@ -364,7 +348,7 @@ func (k Keeper) VerifyNextSequenceRecv( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.NextSequenceRecvPath(portID, channelID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -394,3 +378,19 @@ func (k Keeper) getBlockDelay(ctx sdk.Context, connection exported.ConnectionI) timeDelay := connection.GetDelayPeriod() return uint64(math.Ceil(float64(timeDelay) / float64(expectedTimePerBlock))) } + +// getClientStateAndVerificationStore returns the client state and associated KVStore for the provided client identifier. +// If the client type is localhost then the core IBC KVStore is returned, otherwise the client prefixed store is returned. +func (k Keeper) getClientStateAndVerificationStore(ctx sdk.Context, clientID string) (exported.ClientState, sdk.KVStore, error) { + clientState, found := k.clientKeeper.GetClientState(ctx, clientID) + if !found { + return nil, nil, errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + } + + store := k.clientKeeper.ClientStore(ctx, clientID) + if clientID == exported.LocalhostClientID { + store = ctx.KVStore(k.storeKey) + } + + return clientState, store, nil +} diff --git a/modules/core/03-connection/migrations/v7/expected_keepers.go b/modules/core/03-connection/migrations/v7/expected_keepers.go new file mode 100644 index 00000000000..8427e2cc2bf --- /dev/null +++ b/modules/core/03-connection/migrations/v7/expected_keepers.go @@ -0,0 +1,10 @@ +package v7 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ConnectionKeeper expected IBC connection keeper +type ConnectionKeeper interface { + CreateSentinelLocalhostConnection(ctx sdk.Context) +} diff --git a/modules/core/03-connection/migrations/v7/localhost.go b/modules/core/03-connection/migrations/v7/localhost.go new file mode 100644 index 00000000000..76f768402de --- /dev/null +++ b/modules/core/03-connection/migrations/v7/localhost.go @@ -0,0 +1,11 @@ +package v7 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MigrateLocalhostConnection creates the sentinel localhost connection end to enable +// localhost ibc functionality. +func MigrateLocalhostConnection(ctx sdk.Context, connectionKeeper ConnectionKeeper) { + connectionKeeper.CreateSentinelLocalhostConnection(ctx) +} diff --git a/modules/core/03-connection/types/expected_keepers.go b/modules/core/03-connection/types/expected_keepers.go index 5945766726d..d9b8dec6599 100644 --- a/modules/core/03-connection/types/expected_keepers.go +++ b/modules/core/03-connection/types/expected_keepers.go @@ -8,6 +8,7 @@ import ( // ClientKeeper expected account IBC client keeper type ClientKeeper interface { + GetClientStatus(ctx sdk.Context, clientState exported.ClientState, clientID string) exported.Status GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) diff --git a/modules/core/03-connection/types/msgs.go b/modules/core/03-connection/types/msgs.go index d05efe72758..b8a9d1e4e5d 100644 --- a/modules/core/03-connection/types/msgs.go +++ b/modules/core/03-connection/types/msgs.go @@ -44,6 +44,10 @@ func NewMsgConnectionOpenInit( // ValidateBasic implements sdk.Msg. func (msg MsgConnectionOpenInit) ValidateBasic() error { + if msg.ClientId == exported.LocalhostClientID { + return errorsmod.Wrap(clienttypes.ErrInvalidClientType, "localhost connection handshakes are disallowed") + } + if err := host.ClientIdentifierValidator(msg.ClientId); err != nil { return errorsmod.Wrap(err, "invalid client ID") } @@ -103,6 +107,10 @@ func NewMsgConnectionOpenTry( // ValidateBasic implements sdk.Msg func (msg MsgConnectionOpenTry) ValidateBasic() error { + if msg.ClientId == exported.LocalhostClientID { + return errorsmod.Wrap(clienttypes.ErrInvalidClientType, "localhost connection handshakes are disallowed") + } + if msg.PreviousConnectionId != "" { return errorsmod.Wrap(ErrInvalidConnectionIdentifier, "previous connection identifier must be empty, this field has been deprecated as crossing hellos are no longer supported") } diff --git a/modules/core/03-connection/types/msgs_test.go b/modules/core/03-connection/types/msgs_test.go index 0db720e1fc5..61041342bd4 100644 --- a/modules/core/03-connection/types/msgs_test.go +++ b/modules/core/03-connection/types/msgs_test.go @@ -16,6 +16,7 @@ import ( clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/cosmos/ibc-go/v7/testing/simapp" @@ -88,6 +89,7 @@ func (suite *MsgTestSuite) TestNewMsgConnectionOpenInit() { msg *types.MsgConnectionOpenInit expPass bool }{ + {"localhost client ID", types.NewMsgConnectionOpenInit(exported.LocalhostClientID, "clienttotest", prefix, version, 500, signer), false}, {"invalid client ID", types.NewMsgConnectionOpenInit("test/iris", "clienttotest", prefix, version, 500, signer), false}, {"invalid counterparty client ID", types.NewMsgConnectionOpenInit("clienttotest", "(clienttotest)", prefix, version, 500, signer), false}, {"invalid counterparty connection ID", &types.MsgConnectionOpenInit{connectionID, types.NewCounterparty("clienttotest", "connectiontotest", prefix), version, 500, signer}, false}, @@ -134,6 +136,7 @@ func (suite *MsgTestSuite) TestNewMsgConnectionOpenTry() { expPass bool }{ {"non empty connection ID", &types.MsgConnectionOpenTry{"connection-0", "clienttotesta", protoAny, counterparty, 500, []*types.Version{ibctesting.ConnectionVersion}, clientHeight, suite.proof, suite.proof, suite.proof, clientHeight, signer, nil}, false}, + {"localhost client ID", types.NewMsgConnectionOpenTry(exported.LocalhostClientID, "connectiontotest", "clienttotest", clientState, prefix, []*types.Version{ibctesting.ConnectionVersion}, 500, suite.proof, suite.proof, suite.proof, clientHeight, clientHeight, signer), false}, {"invalid client ID", types.NewMsgConnectionOpenTry("test/iris", "connectiontotest", "clienttotest", clientState, prefix, []*types.Version{ibctesting.ConnectionVersion}, 500, suite.proof, suite.proof, suite.proof, clientHeight, clientHeight, signer), false}, {"invalid counterparty connection ID", types.NewMsgConnectionOpenTry("clienttotesta", "ibc/test", "clienttotest", clientState, prefix, []*types.Version{ibctesting.ConnectionVersion}, 500, suite.proof, suite.proof, suite.proof, clientHeight, clientHeight, signer), false}, {"invalid counterparty client ID", types.NewMsgConnectionOpenTry("clienttotesta", "connectiontotest", "test/conn1", clientState, prefix, []*types.Version{ibctesting.ConnectionVersion}, 500, suite.proof, suite.proof, suite.proof, clientHeight, clientHeight, signer), false}, diff --git a/modules/core/04-channel/keeper/events.go b/modules/core/04-channel/keeper/events.go index 81304721b74..07b105f363a 100644 --- a/modules/core/04-channel/keeper/events.go +++ b/modules/core/04-channel/keeper/events.go @@ -138,7 +138,8 @@ func emitSendPacketEvent(ctx sdk.Context, packet exported.PacketI, channel types sdk.NewAttribute(types.AttributeKeyChannelOrdering, channel.Ordering.String()), // we only support 1-hop packets now, and that is the most important hop for a relayer // (is it going to a chain I am connected to) - sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), + sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), //nolint:staticcheck // DEPRECATED + sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -165,7 +166,8 @@ func emitRecvPacketEvent(ctx sdk.Context, packet exported.PacketI, channel types sdk.NewAttribute(types.AttributeKeyChannelOrdering, channel.Ordering.String()), // we only support 1-hop packets now, and that is the most important hop for a relayer // (is it going to a chain I am connected to) - sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), + sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), //nolint:staticcheck // DEPRECATED + sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -192,7 +194,8 @@ func emitWriteAcknowledgementEvent(ctx sdk.Context, packet exported.PacketI, cha sdk.NewAttribute(types.AttributeKeyAckHex, hex.EncodeToString(acknowledgement)), // we only support 1-hop packets now, and that is the most important hop for a relayer // (is it going to a chain I am connected to) - sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), + sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), //nolint:staticcheck // DEPRECATED + sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -217,7 +220,8 @@ func emitAcknowledgePacketEvent(ctx sdk.Context, packet exported.PacketI, channe sdk.NewAttribute(types.AttributeKeyChannelOrdering, channel.Ordering.String()), // we only support 1-hop packets now, and that is the most important hop for a relayer // (is it going to a chain I am connected to) - sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), + sdk.NewAttribute(types.AttributeKeyConnection, channel.ConnectionHops[0]), //nolint:staticcheck // DEPRECATED + sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -239,6 +243,7 @@ func emitTimeoutPacketEvent(ctx sdk.Context, packet exported.PacketI, channel ty sdk.NewAttribute(types.AttributeKeySrcChannel, packet.GetSourceChannel()), sdk.NewAttribute(types.AttributeKeyDstPort, packet.GetDestPort()), sdk.NewAttribute(types.AttributeKeyDstChannel, packet.GetDestChannel()), + sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]), sdk.NewAttribute(types.AttributeKeyChannelOrdering, channel.Ordering.String()), ), sdk.NewEvent( diff --git a/modules/core/04-channel/keeper/handshake.go b/modules/core/04-channel/keeper/handshake.go index 6419a8984bd..cfca1efd00d 100644 --- a/modules/core/04-channel/keeper/handshake.go +++ b/modules/core/04-channel/keeper/handshake.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" @@ -50,6 +51,15 @@ func (k Keeper) ChanOpenInit( ) } + clientState, found := k.clientKeeper.GetClientState(ctx, connectionEnd.ClientId) + if !found { + return "", nil, errorsmod.Wrapf(clienttypes.ErrClientNotFound, "clientID (%s)", connectionEnd.ClientId) + } + + if status := k.clientKeeper.GetClientStatus(ctx, clientState, connectionEnd.ClientId); status != exported.Active { + return "", nil, errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", connectionEnd.ClientId, status) + } + if !k.portKeeper.Authenticate(ctx, portCap, portID) { return "", nil, errorsmod.Wrapf(porttypes.ErrInvalidPort, "caller does not own port capability for port ID %s", portID) } @@ -401,6 +411,15 @@ func (k Keeper) ChanCloseInit( return errorsmod.Wrap(connectiontypes.ErrConnectionNotFound, channel.ConnectionHops[0]) } + clientState, found := k.clientKeeper.GetClientState(ctx, connectionEnd.ClientId) + if !found { + return errorsmod.Wrapf(clienttypes.ErrClientNotFound, "clientID (%s)", connectionEnd.ClientId) + } + + if status := k.clientKeeper.GetClientStatus(ctx, clientState, connectionEnd.ClientId); status != exported.Active { + return errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", connectionEnd.ClientId, status) + } + if connectionEnd.GetState() != int32(connectiontypes.OPEN) { return errorsmod.Wrapf( connectiontypes.ErrInvalidConnectionState, diff --git a/modules/core/04-channel/keeper/handshake_test.go b/modules/core/04-channel/keeper/handshake_test.go index bf77d7ed530..8c721d1f438 100644 --- a/modules/core/04-channel/keeper/handshake_test.go +++ b/modules/core/04-channel/keeper/handshake_test.go @@ -25,9 +25,10 @@ type testCase = struct { // can succeed. func (suite *KeeperTestSuite) TestChanOpenInit() { var ( - path *ibctesting.Path - features []string - portCap *capabilitytypes.Capability + path *ibctesting.Path + features []string + portCap *capabilitytypes.Capability + expErrorMsgSubstring string ) testCases := []testCase{ @@ -85,6 +86,22 @@ func (suite *KeeperTestSuite) TestChanOpenInit() { suite.chainA.CreatePortCapability(suite.chainA.GetSimApp().ScopedIBCMockKeeper, ibctesting.MockPort) portCap = suite.chainA.GetPortCapability(ibctesting.MockPort) }, true}, + { + msg: "unauthorized client", + expPass: false, + malleate: func() { + expErrorMsgSubstring = "status is Unauthorized" + suite.coordinator.SetupConnections(path) + + // remove client from allowed list + params := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetParams(suite.chainA.GetContext()) + params.AllowedClients = []string{} + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetParams(suite.chainA.GetContext(), params) + + suite.chainA.CreatePortCapability(suite.chainA.GetSimApp().ScopedIBCMockKeeper, ibctesting.MockPort) + portCap = suite.chainA.GetPortCapability(ibctesting.MockPort) + }, + }, } for _, tc := range testCases { @@ -96,6 +113,7 @@ func (suite *KeeperTestSuite) TestChanOpenInit() { path = ibctesting.NewPath(suite.chainA, suite.chainB) path.EndpointA.ChannelConfig.Order = order path.EndpointB.ChannelConfig.Order = order + expErrorMsgSubstring = "" tc.malleate() @@ -129,6 +147,7 @@ func (suite *KeeperTestSuite) TestChanOpenInit() { suite.Require().Equal(chanCap.String(), cap.String(), "channel capability is not correct") } else { suite.Require().Error(err) + suite.Require().Contains(err.Error(), expErrorMsgSubstring) suite.Require().Nil(cap) suite.Require().Equal("", channelID) } @@ -598,8 +617,9 @@ func (suite *KeeperTestSuite) TestChanOpenConfirm() { // ChanCloseInit. Both chains will use message passing to setup OPEN channels. func (suite *KeeperTestSuite) TestChanCloseInit() { var ( - path *ibctesting.Path - channelCap *capabilitytypes.Capability + path *ibctesting.Path + channelCap *capabilitytypes.Capability + expErrorMsgSubstring string ) testCases := []testCase{ @@ -655,6 +675,20 @@ func (suite *KeeperTestSuite) TestChanCloseInit() { suite.coordinator.Setup(path) channelCap = capabilitytypes.NewCapability(3) }, false}, + { + msg: "unauthorized client", + expPass: false, + malleate: func() { + suite.coordinator.Setup(path) + channelCap = suite.chainA.GetChannelCapability(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + + // remove client from allowed list + params := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetParams(suite.chainA.GetContext()) + params.AllowedClients = []string{} + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetParams(suite.chainA.GetContext(), params) + expErrorMsgSubstring = "status is Unauthorized" + }, + }, } for _, tc := range testCases { @@ -662,6 +696,7 @@ func (suite *KeeperTestSuite) TestChanCloseInit() { suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { suite.SetupTest() // reset path = ibctesting.NewPath(suite.chainA, suite.chainB) + expErrorMsgSubstring = "" tc.malleate() @@ -673,6 +708,7 @@ func (suite *KeeperTestSuite) TestChanCloseInit() { suite.Require().NoError(err) } else { suite.Require().Error(err) + suite.Require().Contains(err.Error(), expErrorMsgSubstring) } }) } diff --git a/modules/core/04-channel/keeper/packet.go b/modules/core/04-channel/keeper/packet.go index c69cdfd6554..59be12b73ec 100644 --- a/modules/core/04-channel/keeper/packet.go +++ b/modules/core/04-channel/keeper/packet.go @@ -71,8 +71,7 @@ func (k Keeper) SendPacket( } // prevent accidental sends with clients that cannot be updated - clientStore := k.clientKeeper.ClientStore(ctx, connectionEnd.GetClientID()) - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := k.clientKeeper.GetClientStatus(ctx, clientState, connectionEnd.GetClientID()); status != exported.Active { return 0, errorsmod.Wrapf(clienttypes.ErrClientNotActive, "cannot send packet using client (%s) with status %s", connectionEnd.GetClientID(), status) } diff --git a/modules/core/04-channel/types/expected_keepers.go b/modules/core/04-channel/types/expected_keepers.go index 3a5460502c6..3c0b0b0198d 100644 --- a/modules/core/04-channel/types/expected_keepers.go +++ b/modules/core/04-channel/types/expected_keepers.go @@ -10,6 +10,7 @@ import ( // ClientKeeper expected account IBC client keeper type ClientKeeper interface { + GetClientStatus(ctx sdk.Context, clientState exported.ClientState, clientID string) exported.Status GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) ClientStore(ctx sdk.Context, clientID string) sdk.KVStore diff --git a/modules/core/04-channel/types/msgs.go b/modules/core/04-channel/types/msgs.go index fec50b53b69..eab7f00bba5 100644 --- a/modules/core/04-channel/types/msgs.go +++ b/modules/core/04-channel/types/msgs.go @@ -94,7 +94,7 @@ func (msg MsgChannelOpenTry) ValidateBasic() error { return errorsmod.Wrap(ErrInvalidChannelIdentifier, "previous channel identifier must be empty, this field has been deprecated as crossing hellos are no longer supported") } if len(msg.ProofInit) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof init") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty init proof") } if msg.Channel.State != TRYOPEN { return errorsmod.Wrapf(ErrInvalidChannelState, @@ -155,7 +155,7 @@ func (msg MsgChannelOpenAck) ValidateBasic() error { return errorsmod.Wrap(err, "invalid counterparty channel ID") } if len(msg.ProofTry) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof try") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty try proof") } _, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { @@ -200,7 +200,7 @@ func (msg MsgChannelOpenConfirm) ValidateBasic() error { return ErrInvalidChannelIdentifier } if len(msg.ProofAck) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof ack") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty acknowledgement proof") } _, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { @@ -284,7 +284,7 @@ func (msg MsgChannelCloseConfirm) ValidateBasic() error { return ErrInvalidChannelIdentifier } if len(msg.ProofInit) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof init") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty init proof") } _, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { @@ -322,7 +322,7 @@ func NewMsgRecvPacket( // ValidateBasic implements sdk.Msg func (msg MsgRecvPacket) ValidateBasic() error { if len(msg.ProofCommitment) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty commitment proof") } _, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { @@ -413,7 +413,7 @@ func (msg MsgTimeoutOnClose) ValidateBasic() error { return errorsmod.Wrap(ibcerrors.ErrInvalidSequence, "next sequence receive cannot be 0") } if len(msg.ProofUnreceived) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty unreceived proof") } if len(msg.ProofClose) == 0 { return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof of closed counterparty channel end") @@ -457,7 +457,7 @@ func NewMsgAcknowledgement( // ValidateBasic implements sdk.Msg func (msg MsgAcknowledgement) ValidateBasic() error { if len(msg.ProofAcked) == 0 { - return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty proof") + return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "cannot submit an empty acknowledgement proof") } if len(msg.Acknowledgement) == 0 { return errorsmod.Wrap(ErrInvalidAcknowledgement, "ack bytes cannot be empty") diff --git a/modules/core/04-channel/types/msgs_test.go b/modules/core/04-channel/types/msgs_test.go index bd8c5ae2198..0d459dde865 100644 --- a/modules/core/04-channel/types/msgs_test.go +++ b/modules/core/04-channel/types/msgs_test.go @@ -316,8 +316,8 @@ func (suite *TypesTestSuite) TestMsgRecvPacketValidateBasic() { expPass bool }{ {"success", types.NewMsgRecvPacket(packet, suite.proof, height, addr), true}, - {"proof contain empty proof", types.NewMsgRecvPacket(packet, emptyProof, height, addr), false}, {"missing signer address", types.NewMsgRecvPacket(packet, suite.proof, height, emptyAddr), false}, + {"proof contain empty proof", types.NewMsgRecvPacket(packet, emptyProof, height, addr), false}, {"invalid packet", types.NewMsgRecvPacket(invalidPacket, suite.proof, height, addr), false}, } @@ -380,9 +380,9 @@ func (suite *TypesTestSuite) TestMsgTimeoutOnCloseValidateBasic() { }{ {"success", types.NewMsgTimeoutOnClose(packet, 1, suite.proof, suite.proof, height, addr), true}, {"seq 0", types.NewMsgTimeoutOnClose(packet, 0, suite.proof, suite.proof, height, addr), false}, + {"signer address is empty", types.NewMsgTimeoutOnClose(packet, 1, suite.proof, suite.proof, height, emptyAddr), false}, {"empty proof", types.NewMsgTimeoutOnClose(packet, 1, emptyProof, suite.proof, height, addr), false}, {"empty proof close", types.NewMsgTimeoutOnClose(packet, 1, suite.proof, emptyProof, height, addr), false}, - {"signer address is empty", types.NewMsgTimeoutOnClose(packet, 1, suite.proof, suite.proof, height, emptyAddr), false}, {"invalid packet", types.NewMsgTimeoutOnClose(invalidPacket, 1, suite.proof, suite.proof, height, addr), false}, } diff --git a/modules/core/exported/client.go b/modules/core/exported/client.go index 88273ecc860..731d7e5edf7 100644 --- a/modules/core/exported/client.go +++ b/modules/core/exported/client.go @@ -19,6 +19,12 @@ const ( // Tendermint is used to indicate that the client uses the Tendermint Consensus Algorithm. Tendermint string = "07-tendermint" + // Localhost is the client type for the localhost client. + Localhost string = "09-localhost" + + // LocalhostClientID is the sentinel client ID for the localhost client. + LocalhostClientID string = Localhost + // Active is a status type of a client. An active client is allowed to be used. Active Status = "Active" @@ -30,6 +36,9 @@ const ( // Unknown indicates there was an error in determining the status of a client. Unknown Status = "Unknown" + + // Unauthorized indicates that the client type is not registered as an allowed client type. + Unauthorized Status = "Unauthorized" ) // ClientState defines the required common functions for light clients. diff --git a/modules/core/exported/connection.go b/modules/core/exported/connection.go index 93129050db2..c418e8497a7 100644 --- a/modules/core/exported/connection.go +++ b/modules/core/exported/connection.go @@ -1,5 +1,8 @@ package exported +// LocalhostConnectionID is the sentinel connection ID for the localhost connection. +const LocalhostConnectionID string = "connection-localhost" + // ConnectionI describes the required methods for a connection. type ConnectionI interface { GetClientID() string diff --git a/modules/core/module.go b/modules/core/module.go index bc12019da69..7f0cbfbd0aa 100644 --- a/modules/core/module.go +++ b/modules/core/module.go @@ -18,6 +18,7 @@ import ( ibcclient "github.com/cosmos/ibc-go/v7/modules/core/02-client" clientkeeper "github.com/cosmos/ibc-go/v7/modules/core/02-client/keeper" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + connectionkeeper "github.com/cosmos/ibc-go/v7/modules/core/03-connection/keeper" connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" "github.com/cosmos/ibc-go/v7/modules/core/client/cli" @@ -123,9 +124,23 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { channeltypes.RegisterMsgServer(cfg.MsgServer(), am.keeper) types.RegisterQueryService(cfg.QueryServer(), am.keeper) - m := clientkeeper.NewMigrator(am.keeper.ClientKeeper) - err := cfg.RegisterMigration(exported.ModuleName, 2, m.Migrate2to3) - if err != nil { + clientMigrator := clientkeeper.NewMigrator(am.keeper.ClientKeeper) + if err := cfg.RegisterMigration(exported.ModuleName, 2, clientMigrator.Migrate2to3); err != nil { + panic(err) + } + + connectionMigrator := connectionkeeper.NewMigrator(am.keeper.ConnectionKeeper) + if err := cfg.RegisterMigration(exported.ModuleName, 3, func(ctx sdk.Context) error { + if err := connectionMigrator.Migrate3to4(ctx); err != nil { + return err + } + + if err := clientMigrator.Migrate3to4(ctx); err != nil { + return err + } + + return nil + }); err != nil { panic(err) } } @@ -149,7 +164,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 3 } +func (AppModule) ConsensusVersion() uint64 { return 4 } // BeginBlock returns the begin blocker for the ibc module. func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { diff --git a/modules/core/types/codec.go b/modules/core/types/codec.go index 447b05c849b..e2c67424026 100644 --- a/modules/core/types/codec.go +++ b/modules/core/types/codec.go @@ -7,12 +7,15 @@ import ( connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" ) -// RegisterInterfaces registers x/ibc interfaces into protobuf Any. +// RegisterInterfaces registers ibc types against interfaces using the global InterfaceRegistry. +// Note: The localhost client is created by ibc core and thus requires explicit type registration. func RegisterInterfaces(registry codectypes.InterfaceRegistry) { clienttypes.RegisterInterfaces(registry) connectiontypes.RegisterInterfaces(registry) channeltypes.RegisterInterfaces(registry) commitmenttypes.RegisterInterfaces(registry) + localhost.RegisterInterfaces(registry) } diff --git a/modules/light-clients/07-tendermint/client_state_test.go b/modules/light-clients/07-tendermint/client_state_test.go index 2a70d4199a6..9df8c6047ba 100644 --- a/modules/light-clients/07-tendermint/client_state_test.go +++ b/modules/light-clients/07-tendermint/client_state_test.go @@ -420,6 +420,12 @@ func (suite *TendermintTestSuite) TestVerifyMembership() { value = []byte("invalid value") }, false, }, + { + "proof is empty", func() { + // change the inserted proof + proof = []byte{} + }, false, + }, } for _, tc := range testCases { @@ -631,6 +637,12 @@ func (suite *TendermintTestSuite) TestVerifyNonMembership() { proof, proofHeight = suite.chainB.QueryProof(key) }, false, }, + { + "proof is empty", func() { + // change the inserted proof + proof = []byte{} + }, false, + }, } for _, tc := range testCases { diff --git a/modules/light-clients/09-localhost/client_state.go b/modules/light-clients/09-localhost/client_state.go new file mode 100644 index 00000000000..c950c106f7f --- /dev/null +++ b/modules/light-clients/09-localhost/client_state.go @@ -0,0 +1,199 @@ +package localhost + +import ( + "bytes" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + ibcerrors "github.com/cosmos/ibc-go/v7/internal/errors" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v7/modules/core/24-host" + "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +var _ exported.ClientState = (*ClientState)(nil) + +// NewClientState creates a new 09-localhost ClientState instance. +func NewClientState(height clienttypes.Height) exported.ClientState { + return &ClientState{ + LatestHeight: height, + } +} + +// ClientType returns the 09-localhost client type. +func (cs ClientState) ClientType() string { + return exported.Localhost +} + +// GetLatestHeight returns the 09-localhost client state latest height. +func (cs ClientState) GetLatestHeight() exported.Height { + return cs.LatestHeight +} + +// Status always returns Active. The 09-localhost status cannot be changed. +func (cs ClientState) Status(_ sdk.Context, _ sdk.KVStore, _ codec.BinaryCodec) exported.Status { + return exported.Active +} + +// Validate performs a basic validation of the client state fields. +func (cs ClientState) Validate() error { + if cs.LatestHeight.RevisionHeight == 0 { + return errorsmod.Wrapf(ibcerrors.ErrInvalidHeight, "local revision height cannot be zero") + } + + return nil +} + +// ZeroCustomFields returns the same client state since there are no custom fields in the 09-localhost client state. +func (cs ClientState) ZeroCustomFields() exported.ClientState { + return &cs +} + +// Initialize ensures that initial consensus state for localhost is nil. +func (cs ClientState) Initialize(ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, consState exported.ConsensusState) error { + if consState != nil { + return errorsmod.Wrap(clienttypes.ErrInvalidConsensus, "initial consensus state for localhost must be nil.") + } + + clientState := ClientState{ + LatestHeight: clienttypes.GetSelfHeight(ctx), + } + + clientStore.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(cdc, &clientState)) + + return nil +} + +// GetTimestampAtHeight returns the current block time retrieved from the application context. The localhost client does not store consensus states and thus +// cannot provide a timestamp for the provided height. +func (cs ClientState) GetTimestampAtHeight(ctx sdk.Context, _ sdk.KVStore, _ codec.BinaryCodec, _ exported.Height) (uint64, error) { + return uint64(ctx.BlockTime().UnixNano()), nil +} + +// VerifyMembership is a generic proof verification method which verifies the existence of a given key and value within the IBC store. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). +// The caller must provide the full IBC store. +func (cs ClientState) VerifyMembership( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + proof []byte, + path exported.Path, + value []byte, +) error { + // ensure the proof provided is the expected sentintel localhost client proof + if !bytes.Equal(proof, SentinelProof) { + return errorsmod.Wrapf(commitmenttypes.ErrInvalidProof, "expected %s, got %s", string(SentinelProof), string(proof)) + } + + merklePath, ok := path.(commitmenttypes.MerklePath) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) + } + + if len(merklePath.GetKeyPath()) != 2 { + return errorsmod.Wrapf(host.ErrInvalidPath, "path must be of length 2: %s", merklePath.GetKeyPath()) + } + + // The commitment prefix (eg: "ibc") is omitted when operating on the core IBC store + bz := store.Get([]byte(merklePath.KeyPath[1])) + if bz == nil { + return errorsmod.Wrapf(clienttypes.ErrFailedMembershipVerification, "value not found for path %s", path) + } + + if !bytes.Equal(bz, value) { + return errorsmod.Wrapf(clienttypes.ErrFailedMembershipVerification, "value provided does not equal value stored at path: %s", path) + } + + return nil +} + +// VerifyNonMembership is a generic proof verification method which verifies the absence of a given CommitmentPath within the IBC store. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). +// The caller must provide the full IBC store. +func (cs ClientState) VerifyNonMembership( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + proof []byte, + path exported.Path, +) error { + // ensure the proof provided is the expected sentintel localhost client proof + if !bytes.Equal(proof, SentinelProof) { + return errorsmod.Wrapf(commitmenttypes.ErrInvalidProof, "expected %s, got %s", string(SentinelProof), string(proof)) + } + + merklePath, ok := path.(commitmenttypes.MerklePath) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) + } + + if len(merklePath.GetKeyPath()) != 2 { + return errorsmod.Wrapf(host.ErrInvalidPath, "path must be of length 2: %s", merklePath.GetKeyPath()) + } + + // The commitment prefix (eg: "ibc") is omitted when operating on the core IBC store + if store.Has([]byte(merklePath.KeyPath[1])) { + return errorsmod.Wrapf(clienttypes.ErrFailedNonMembershipVerification, "value found for path %s", path) + } + + return nil +} + +// VerifyClientMessage is unsupported by the 09-localhost client type and returns an error. +func (cs ClientState) VerifyClientMessage(_ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage) error { + return errorsmod.Wrap(clienttypes.ErrUpdateClientFailed, "client message verification is unsupported by the localhost client") +} + +// CheckForMisbehaviour is unsupported by the 09-localhost client type and performs a no-op, returning false. +func (cs ClientState) CheckForMisbehaviour(_ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage) bool { + return false +} + +// UpdateStateOnMisbehaviour is unsupported by the 09-localhost client type and performs a no-op. +func (cs ClientState) UpdateStateOnMisbehaviour(_ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage) { +} + +// UpdateState updates and stores as necessary any associated information for an IBC client, such as the ClientState and corresponding ConsensusState. +// Upon successful update, a list of consensus heights is returned. It assumes the ClientMessage has already been verified. +func (cs ClientState) UpdateState(ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, _ exported.ClientMessage) []exported.Height { + height := clienttypes.GetSelfHeight(ctx) + cs.LatestHeight = height + + clientStore.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(cdc, &cs)) + + return []exported.Height{height} +} + +// ExportMetadata is a no-op for the 09-localhost client. +func (cs ClientState) ExportMetadata(_ sdk.KVStore) []exported.GenesisMetadata { + return nil +} + +// CheckSubstituteAndUpdateState returns an error. The localhost cannot be modified by +// proposals. +func (cs ClientState) CheckSubstituteAndUpdateState(_ sdk.Context, _ codec.BinaryCodec, _, _ sdk.KVStore, _ exported.ClientState) error { + return errorsmod.Wrap(clienttypes.ErrUpdateClientFailed, "cannot update localhost client with a proposal") +} + +// VerifyUpgradeAndUpdateState returns an error since localhost cannot be upgraded +func (cs ClientState) VerifyUpgradeAndUpdateState( + _ sdk.Context, + _ codec.BinaryCodec, + _ sdk.KVStore, + _ exported.ClientState, + _ exported.ConsensusState, + _, + _ []byte, +) error { + return errorsmod.Wrap(clienttypes.ErrInvalidUpgradeClient, "cannot upgrade localhost client") +} diff --git a/modules/light-clients/09-localhost/client_state_test.go b/modules/light-clients/09-localhost/client_state_test.go new file mode 100644 index 00000000000..b11034a87e3 --- /dev/null +++ b/modules/light-clients/09-localhost/client_state_test.go @@ -0,0 +1,434 @@ +package localhost_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v7/modules/core/24-host" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + localhost "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost" + ibctesting "github.com/cosmos/ibc-go/v7/testing" + "github.com/cosmos/ibc-go/v7/testing/mock" +) + +func (suite *LocalhostTestSuite) TestStatus() { + clientState := localhost.NewClientState(clienttypes.NewHeight(3, 10)) + suite.Require().Equal(exported.Active, clientState.Status(suite.chain.GetContext(), nil, nil)) +} + +func (suite *LocalhostTestSuite) TestClientType() { + clientState := localhost.NewClientState(clienttypes.NewHeight(3, 10)) + suite.Require().Equal(exported.Localhost, clientState.ClientType()) +} + +func (suite *LocalhostTestSuite) TestGetLatestHeight() { + expectedHeight := clienttypes.NewHeight(3, 10) + clientState := localhost.NewClientState(expectedHeight) + suite.Require().Equal(expectedHeight, clientState.GetLatestHeight()) +} + +func (suite *LocalhostTestSuite) TestZeroCustomFields() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + suite.Require().Equal(clientState, clientState.ZeroCustomFields()) +} + +func (suite *LocalhostTestSuite) TestGetTimestampAtHeight() { + ctx := suite.chain.GetContext() + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + + timestamp, err := clientState.GetTimestampAtHeight(ctx, nil, nil, nil) + suite.Require().NoError(err) + suite.Require().Equal(uint64(ctx.BlockTime().UnixNano()), timestamp) +} + +func (suite *LocalhostTestSuite) TestValidate() { + testCases := []struct { + name string + clientState exported.ClientState + expPass bool + }{ + { + name: "valid client", + clientState: localhost.NewClientState(clienttypes.NewHeight(3, 10)), + expPass: true, + }, + { + name: "invalid height", + clientState: localhost.NewClientState(clienttypes.ZeroHeight()), + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.clientState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestInitialize() { + testCases := []struct { + name string + consState exported.ConsensusState + expPass bool + }{ + { + "valid initialization", + nil, + true, + }, + { + "invalid consenus state", + &ibctm.ConsensusState{}, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + + clientState := localhost.NewClientState(clienttypes.NewHeight(3, 10)) + clientStore := suite.chain.GetSimApp().GetIBCKeeper().ClientKeeper.ClientStore(suite.chain.GetContext(), exported.LocalhostClientID) + + err := clientState.Initialize(suite.chain.GetContext(), suite.chain.Codec, clientStore, tc.consState) + + if tc.expPass { + suite.Require().NoError(err, "valid testcase: %s failed", tc.name) + } else { + suite.Require().Error(err, "invalid testcase: %s passed", tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyMembership() { + var ( + path exported.Path + value []byte + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success: client state verification", + func() { + clientState := suite.chain.GetClientState(exported.LocalhostClientID) + + merklePath := commitmenttypes.NewMerklePath(host.FullClientStatePath(exported.LocalhostClientID)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = clienttypes.MustMarshalClientState(suite.chain.Codec, clientState) + }, + true, + }, + { + "success: connection state verification", + func() { + connectionEnd := connectiontypes.NewConnectionEnd( + connectiontypes.OPEN, + exported.LocalhostClientID, + connectiontypes.NewCounterparty(exported.LocalhostClientID, exported.LocalhostConnectionID, suite.chain.GetPrefix()), + connectiontypes.ExportedVersionsToProto(connectiontypes.GetCompatibleVersions()), 0, + ) + + suite.chain.GetSimApp().GetIBCKeeper().ConnectionKeeper.SetConnection(suite.chain.GetContext(), exported.LocalhostConnectionID, connectionEnd) + + merklePath := commitmenttypes.NewMerklePath(host.ConnectionPath(exported.LocalhostConnectionID)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = suite.chain.Codec.MustMarshal(&connectionEnd) + }, + true, + }, + { + "success: channel state verification", + func() { + channel := channeltypes.NewChannel( + channeltypes.OPEN, + channeltypes.UNORDERED, + channeltypes.NewCounterparty(mock.PortID, ibctesting.FirstChannelID), + []string{exported.LocalhostConnectionID}, + mock.Version, + ) + + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetChannel(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, channel) + + merklePath := commitmenttypes.NewMerklePath(host.ChannelPath(mock.PortID, ibctesting.FirstChannelID)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = suite.chain.Codec.MustMarshal(&channel) + }, + true, + }, + { + "success: next sequence recv verification", + func() { + nextSeqRecv := uint64(100) + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetNextSequenceRecv(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, nextSeqRecv) + + merklePath := commitmenttypes.NewMerklePath(host.NextSequenceRecvPath(mock.PortID, ibctesting.FirstChannelID)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = sdk.Uint64ToBigEndian(nextSeqRecv) + }, + true, + }, + { + "success: packet commitment verification", + func() { + packet := channeltypes.NewPacket( + ibctesting.MockPacketData, + 1, + ibctesting.MockPort, + ibctesting.FirstChannelID, + ibctesting.MockPort, + ibctesting.FirstChannelID, + clienttypes.NewHeight(0, 10), + 0, + ) + + commitmentBz := channeltypes.CommitPacket(suite.chain.Codec, packet) + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetPacketCommitment(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, 1, commitmentBz) + + merklePath := commitmenttypes.NewMerklePath(host.PacketCommitmentPath(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = commitmentBz + }, + true, + }, + { + "success: packet acknowledgement verification", + func() { + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetPacketAcknowledgement(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, 1, ibctesting.MockAcknowledgement) + + merklePath := commitmenttypes.NewMerklePath(host.PacketAcknowledgementPath(mock.PortID, ibctesting.FirstChannelID, 1)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = ibctesting.MockAcknowledgement + }, + true, + }, + { + "invalid type for key path", + func() { + path = mock.KeyPath{} + }, + false, + }, + { + "key path has too many elements", + func() { + path = commitmenttypes.NewMerklePath("ibc", "test", "key") + }, + false, + }, + { + "no value found at provided key path", + func() { + merklePath := commitmenttypes.NewMerklePath(host.PacketAcknowledgementPath(mock.PortID, ibctesting.FirstChannelID, 100)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + value = ibctesting.MockAcknowledgement + }, + false, + }, + { + "invalid value, bytes are not equal", + func() { + channel := channeltypes.NewChannel( + channeltypes.OPEN, + channeltypes.UNORDERED, + channeltypes.NewCounterparty(mock.PortID, ibctesting.FirstChannelID), + []string{exported.LocalhostConnectionID}, + mock.Version, + ) + + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetChannel(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, channel) + + merklePath := commitmenttypes.NewMerklePath(host.ChannelPath(mock.PortID, ibctesting.FirstChannelID)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + + channel.State = channeltypes.CLOSED // modify the channel before marshalling to value bz + value = suite.chain.Codec.MustMarshal(&channel) + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + tc.malleate() + + clientState := suite.chain.GetClientState(exported.LocalhostClientID) + store := suite.chain.GetContext().KVStore(suite.chain.GetSimApp().GetKey(exported.StoreKey)) + + err := clientState.VerifyMembership( + suite.chain.GetContext(), + store, + suite.chain.Codec, + clienttypes.ZeroHeight(), + 0, 0, // use zero values for delay periods + localhost.SentinelProof, + path, + value, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyNonMembership() { + var path exported.Path + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success: packet receipt absence verification", + func() { + merklePath := commitmenttypes.NewMerklePath(host.PacketReceiptPath(mock.PortID, ibctesting.FirstChannelID, 1)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + }, + true, + }, + { + "packet receipt absence verification fails", + func() { + suite.chain.GetSimApp().GetIBCKeeper().ChannelKeeper.SetPacketReceipt(suite.chain.GetContext(), mock.PortID, ibctesting.FirstChannelID, 1) + + merklePath := commitmenttypes.NewMerklePath(host.PacketReceiptPath(mock.PortID, ibctesting.FirstChannelID, 1)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chain.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path = merklePath + }, + false, + }, + { + "invalid type for key path", + func() { + path = mock.KeyPath{} + }, + false, + }, + { + "key path has too many elements", + func() { + path = commitmenttypes.NewMerklePath("ibc", "test", "key") + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + tc.malleate() + + clientState := suite.chain.GetClientState(exported.LocalhostClientID) + store := suite.chain.GetContext().KVStore(suite.chain.GetSimApp().GetKey(exported.StoreKey)) + + err := clientState.VerifyNonMembership( + suite.chain.GetContext(), + store, + suite.chain.Codec, + clienttypes.ZeroHeight(), + 0, 0, // use zero values for delay periods + localhost.SentinelProof, + path, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyClientMessage() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + suite.Require().Error(clientState.VerifyClientMessage(suite.chain.GetContext(), nil, nil, nil)) +} + +func (suite *LocalhostTestSuite) TestVerifyCheckForMisbehaviour() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + suite.Require().False(clientState.CheckForMisbehaviour(suite.chain.GetContext(), nil, nil, nil)) +} + +func (suite *LocalhostTestSuite) TestUpdateState() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, uint64(suite.chain.GetContext().BlockHeight()))) + store := suite.chain.GetSimApp().GetIBCKeeper().ClientKeeper.ClientStore(suite.chain.GetContext(), exported.LocalhostClientID) + + suite.coordinator.CommitBlock(suite.chain) + + heights := clientState.UpdateState(suite.chain.GetContext(), suite.chain.Codec, store, nil) + + expHeight := clienttypes.NewHeight(1, uint64(suite.chain.GetContext().BlockHeight())) + suite.Require().True(heights[0].EQ(expHeight)) + + clientState = suite.chain.GetClientState(exported.LocalhostClientID) + suite.Require().True(heights[0].EQ(clientState.GetLatestHeight())) +} + +func (suite *LocalhostTestSuite) TestExportMetadata() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + suite.Require().Nil(clientState.ExportMetadata(nil)) +} + +func (suite *LocalhostTestSuite) TestCheckSubstituteAndUpdateState() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + err := clientState.CheckSubstituteAndUpdateState(suite.chain.GetContext(), suite.chain.Codec, nil, nil, nil) + suite.Require().Error(err) +} + +func (suite *LocalhostTestSuite) TestVerifyUpgradeAndUpdateState() { + clientState := localhost.NewClientState(clienttypes.NewHeight(1, 10)) + err := clientState.VerifyUpgradeAndUpdateState(suite.chain.GetContext(), suite.chain.Codec, nil, nil, nil, nil, nil) + suite.Require().Error(err) +} diff --git a/modules/light-clients/09-localhost/codec.go b/modules/light-clients/09-localhost/codec.go new file mode 100644 index 00000000000..04803e7df19 --- /dev/null +++ b/modules/light-clients/09-localhost/codec.go @@ -0,0 +1,16 @@ +package localhost + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + + "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +// RegisterInterfaces registers the tendermint concrete client-related +// implementations and interfaces. +func RegisterInterfaces(registry codectypes.InterfaceRegistry) { + registry.RegisterImplementations( + (*exported.ClientState)(nil), + &ClientState{}, + ) +} diff --git a/modules/light-clients/09-localhost/keys.go b/modules/light-clients/09-localhost/keys.go new file mode 100644 index 00000000000..bf10ed0d800 --- /dev/null +++ b/modules/light-clients/09-localhost/keys.go @@ -0,0 +1,12 @@ +package localhost + +const ( + // ModuleName defines the 09-localhost light client module name + ModuleName = "09-localhost" +) + +// SentinelProof defines the 09-localhost sentinel proof. +// Submission of nil or empty proofs is disallowed in core IBC messaging. +// This serves as a placeholder value for relayers to leverage as the proof field in various message types. +// Localhost client state verification will fail if the sentintel proof value is not provided. +var SentinelProof = []byte{0x01} diff --git a/modules/light-clients/09-localhost/localhost.pb.go b/modules/light-clients/09-localhost/localhost.pb.go new file mode 100644 index 00000000000..09303d4f1b2 --- /dev/null +++ b/modules/light-clients/09-localhost/localhost.pb.go @@ -0,0 +1,322 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: ibc/lightclients/localhost/v2/localhost.proto + +package localhost + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + types "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// ClientState defines the 09-localhost client state +type ClientState struct { + // the latest block height + LatestHeight types.Height `protobuf:"bytes,1,opt,name=latest_height,json=latestHeight,proto3" json:"latest_height"` +} + +func (m *ClientState) Reset() { *m = ClientState{} } +func (m *ClientState) String() string { return proto.CompactTextString(m) } +func (*ClientState) ProtoMessage() {} +func (*ClientState) Descriptor() ([]byte, []int) { + return fileDescriptor_60e51cfed1fd7859, []int{0} +} +func (m *ClientState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ClientState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ClientState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ClientState) XXX_Merge(src proto.Message) { + xxx_messageInfo_ClientState.Merge(m, src) +} +func (m *ClientState) XXX_Size() int { + return m.Size() +} +func (m *ClientState) XXX_DiscardUnknown() { + xxx_messageInfo_ClientState.DiscardUnknown(m) +} + +var xxx_messageInfo_ClientState proto.InternalMessageInfo + +func init() { + proto.RegisterType((*ClientState)(nil), "ibc.lightclients.localhost.v2.ClientState") +} + +func init() { + proto.RegisterFile("ibc/lightclients/localhost/v2/localhost.proto", fileDescriptor_60e51cfed1fd7859) +} + +var fileDescriptor_60e51cfed1fd7859 = []byte{ + // 257 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xcd, 0x4c, 0x4a, 0xd6, + 0xcf, 0xc9, 0x4c, 0xcf, 0x28, 0x49, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0x29, 0xd6, 0xcf, 0xc9, 0x4f, + 0x4e, 0xcc, 0xc9, 0xc8, 0x2f, 0x2e, 0xd1, 0x2f, 0x33, 0x42, 0x70, 0xf4, 0x0a, 0x8a, 0xf2, 0x4b, + 0xf2, 0x85, 0x64, 0x33, 0x93, 0x92, 0xf5, 0x90, 0x95, 0xeb, 0x21, 0x54, 0x94, 0x19, 0x49, 0xc9, + 0x83, 0x4c, 0x4b, 0xce, 0x2f, 0x4a, 0xd5, 0x87, 0x48, 0xeb, 0x97, 0x19, 0x42, 0x59, 0x10, 0xfd, + 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0xa6, 0x3e, 0x88, 0x05, 0x11, 0x55, 0x8a, 0xe2, 0xe2, + 0x76, 0x06, 0xab, 0x0a, 0x2e, 0x49, 0x2c, 0x49, 0x15, 0x72, 0xe5, 0xe2, 0xcd, 0x49, 0x2c, 0x49, + 0x2d, 0x2e, 0x89, 0xcf, 0x48, 0x05, 0x59, 0x25, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa5, + 0x07, 0xb2, 0x1c, 0x64, 0xba, 0x1e, 0xd4, 0xcc, 0x32, 0x43, 0x3d, 0x0f, 0xb0, 0x0a, 0x27, 0x96, + 0x13, 0xf7, 0xe4, 0x19, 0x82, 0x78, 0x20, 0xda, 0x20, 0x62, 0x56, 0x2c, 0x1d, 0x0b, 0xe4, 0x19, + 0x9c, 0x92, 0x4e, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, + 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0xca, 0x23, 0x3d, 0xb3, + 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x3f, 0x39, 0xbf, 0x38, 0x37, 0xbf, 0x58, 0x3f, + 0x33, 0x29, 0x59, 0x37, 0x3d, 0x5f, 0xbf, 0xcc, 0x5c, 0x3f, 0x37, 0x3f, 0xa5, 0x34, 0x27, 0xb5, + 0x18, 0x12, 0x34, 0xba, 0xb0, 0xb0, 0x31, 0xb0, 0xd4, 0x85, 0xfb, 0xd7, 0x1a, 0xce, 0x4a, 0x62, + 0x03, 0x7b, 0xc3, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x1e, 0xed, 0x07, 0xba, 0x4d, 0x01, 0x00, + 0x00, +} + +func (m *ClientState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ClientState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ClientState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.LatestHeight.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintLocalhost(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintLocalhost(dAtA []byte, offset int, v uint64) int { + offset -= sovLocalhost(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *ClientState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.LatestHeight.Size() + n += 1 + l + sovLocalhost(uint64(l)) + return n +} + +func sovLocalhost(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozLocalhost(x uint64) (n int) { + return sovLocalhost(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *ClientState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLocalhost + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ClientState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ClientState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LatestHeight", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLocalhost + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLocalhost + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLocalhost + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.LatestHeight.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLocalhost(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthLocalhost + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipLocalhost(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLocalhost + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLocalhost + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLocalhost + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthLocalhost + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupLocalhost + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthLocalhost + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthLocalhost = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowLocalhost = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupLocalhost = fmt.Errorf("proto: unexpected end of group") +) diff --git a/modules/light-clients/09-localhost/localhost_test.go b/modules/light-clients/09-localhost/localhost_test.go new file mode 100644 index 00000000000..c22ef1e35ae --- /dev/null +++ b/modules/light-clients/09-localhost/localhost_test.go @@ -0,0 +1,25 @@ +package localhost_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + ibctesting "github.com/cosmos/ibc-go/v7/testing" +) + +type LocalhostTestSuite struct { + suite.Suite + + coordinator ibctesting.Coordinator + chain *ibctesting.TestChain +} + +func (suite *LocalhostTestSuite) SetupTest() { + suite.coordinator = *ibctesting.NewCoordinator(suite.T(), 1) + suite.chain = suite.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func TestLocalhostTestSuite(t *testing.T) { + suite.Run(t, new(LocalhostTestSuite)) +} diff --git a/proto/ibc/core/client/v1/client.proto b/proto/ibc/core/client/v1/client.proto index 266649ba01c..15b47e55548 100644 --- a/proto/ibc/core/client/v1/client.proto +++ b/proto/ibc/core/client/v1/client.proto @@ -98,6 +98,8 @@ message Height { // Params defines the set of IBC light client parameters. message Params { - // allowed_clients defines the list of allowed client state types. + // allowed_clients defines the list of allowed client state types which can be created + // and interacted with. If a client type is removed from the allowed clients list, usage + // of this client will be disabled until it is added again to the list. repeated string allowed_clients = 1 [(gogoproto.moretags) = "yaml:\"allowed_clients\""]; } diff --git a/proto/ibc/lightclients/localhost/v2/localhost.proto b/proto/ibc/lightclients/localhost/v2/localhost.proto new file mode 100644 index 00000000000..ec970eb0008 --- /dev/null +++ b/proto/ibc/lightclients/localhost/v2/localhost.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package ibc.lightclients.localhost.v2; + +option go_package = "github.com/cosmos/ibc-go/v7/modules/light-clients/09-localhost;localhost"; + +import "ibc/core/client/v1/client.proto"; +import "gogoproto/gogo.proto"; + +// ClientState defines the 09-localhost client state +message ClientState { + option (gogoproto.goproto_getters) = false; + + // the latest block height + ibc.core.client.v1.Height latest_height = 1 [(gogoproto.nullable) = false]; +} diff --git a/testing/simapp/app.go b/testing/simapp/app.go index f1d21478510..b2e17de7232 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -119,9 +119,7 @@ import ( ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" ibcmock "github.com/cosmos/ibc-go/v7/testing/mock" simappparams "github.com/cosmos/ibc-go/v7/testing/simapp/params" - simappupgrades "github.com/cosmos/ibc-go/v7/testing/simapp/upgrades" - v6 "github.com/cosmos/ibc-go/v7/testing/simapp/upgrades/v6" - v7 "github.com/cosmos/ibc-go/v7/testing/simapp/upgrades/v7" + "github.com/cosmos/ibc-go/v7/testing/simapp/upgrades" ibctestingtypes "github.com/cosmos/ibc-go/v7/testing/types" ) @@ -920,16 +918,16 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino // setupUpgradeHandlers sets all necessary upgrade handlers for testing purposes func (app *SimApp) setupUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( - simappupgrades.DefaultUpgradeName, - simappupgrades.CreateDefaultUpgradeHandler(app.mm, app.configurator), + upgrades.V5, + upgrades.CreateDefaultUpgradeHandler(app.mm, app.configurator), ) // NOTE: The moduleName arg of v6.CreateUpgradeHandler refers to the auth module ScopedKeeper name to which the channel capability should be migrated from. // This should be the same string value provided upon instantiation of the ScopedKeeper with app.CapabilityKeeper.ScopeToModule() // See: https://github.com/cosmos/ibc-go/blob/v6.1.0/testing/simapp/app.go#L310 app.UpgradeKeeper.SetUpgradeHandler( - v6.UpgradeName, - v6.CreateUpgradeHandler( + upgrades.V6, + upgrades.CreateV6UpgradeHandler( app.mm, app.configurator, app.appCodec, @@ -940,8 +938,8 @@ func (app *SimApp) setupUpgradeHandlers() { ) app.UpgradeKeeper.SetUpgradeHandler( - v7.UpgradeName, - v7.CreateUpgradeHandler( + upgrades.V7, + upgrades.CreateV7UpgradeHandler( app.mm, app.configurator, app.appCodec, @@ -950,6 +948,11 @@ func (app *SimApp) setupUpgradeHandlers() { app.ParamsKeeper, ), ) + + app.UpgradeKeeper.SetUpgradeHandler( + upgrades.V7_1, + upgrades.CreateV7LocalhostUpgradeHandler(app.mm, app.configurator, app.IBCKeeper.ClientKeeper), + ) } // setupUpgradeStoreLoaders sets all necessary store loaders required by upgrades. @@ -959,7 +962,7 @@ func (app *SimApp) setupUpgradeStoreLoaders() { tmos.Exit(fmt.Sprintf("failed to read upgrade info from disk %s", err)) } - if upgradeInfo.Name == v7.UpgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + if upgradeInfo.Name == upgrades.V7 && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { storeUpgrades := storetypes.StoreUpgrades{ Added: []string{ consensusparamtypes.StoreKey, diff --git a/testing/simapp/upgrades/upgrades.go b/testing/simapp/upgrades/upgrades.go index adc81349bdd..1adf5eb3e68 100644 --- a/testing/simapp/upgrades/upgrades.go +++ b/testing/simapp/upgrades/upgrades.go @@ -1,14 +1,32 @@ package upgrades import ( + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + consensusparamskeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + v6 "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/migrations/v6" + clientkeeper "github.com/cosmos/ibc-go/v7/modules/core/02-client/keeper" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibctmmigrations "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint/migrations" ) const ( - // DefaultUpgradeName is the default upgrade name used for upgrade tests which do not require special handling. - DefaultUpgradeName = "normal upgrade" + // V5 defines the upgrade name for the ibc-go/v5 upgrade handler. + V5 = "normal upgrade" // NOTE: keeping as "normal upgrade" as existing tags depend on this name + // V6 defines the upgrade name for the ibc-go/v6 upgrade handler. + V6 = "v6" + // V7 defines the upgrade name for the ibc-go/v7 upgrade handler. + V7 = "v7" + // V7_1 defines the upgrade name for the ibc-go/v7.1 upgrade handler. + V7_1 = "v7.1" ) // CreateDefaultUpgradeHandler creates an upgrade handler which can be used for regular upgrade tests @@ -21,3 +39,60 @@ func CreateDefaultUpgradeHandler( return mm.RunMigrations(ctx, configurator, vm) } } + +// CreateV6UpgradeHandler creates an upgrade handler for the ibc-go/v6 SimApp upgrade. +// NOTE: The v6.MigrateICS27ChannelCapabiliity function can be omitted if chains do not yet implement an ICS27 controller module +func CreateV6UpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + cdc codec.BinaryCodec, + capabilityStoreKey *storetypes.KVStoreKey, + capabilityKeeper *capabilitykeeper.Keeper, + moduleName string, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + if err := v6.MigrateICS27ChannelCapability(ctx, cdc, capabilityStoreKey, capabilityKeeper, moduleName); err != nil { + return nil, err + } + + return mm.RunMigrations(ctx, configurator, vm) + } +} + +// CreateV7UpgradeHandler creates an upgrade handler for the ibc-go/v7 SimApp upgrade. +func CreateV7UpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + cdc codec.BinaryCodec, + clientKeeper clientkeeper.Keeper, + consensusParamsKeeper consensusparamskeeper.Keeper, + paramsKeeper paramskeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + // OPTIONAL: prune expired tendermint consensus states to save storage space + if _, err := ibctmmigrations.PruneExpiredConsensusStates(ctx, cdc, clientKeeper); err != nil { + return nil, err + } + + legacyBaseAppSubspace := paramsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) + baseapp.MigrateParams(ctx, legacyBaseAppSubspace, &consensusParamsKeeper) + + return mm.RunMigrations(ctx, configurator, vm) + } +} + +// CreateV7LocalhostUpgradeHandler creates an upgrade handler for the ibc-go/v7.1 SimApp upgrade. +func CreateV7LocalhostUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + clientKeeper clientkeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + // explicitly update the IBC 02-client params, adding the localhost client type + params := clientKeeper.GetParams(ctx) + params.AllowedClients = append(params.AllowedClients, exported.Localhost) + clientKeeper.SetParams(ctx, params) + + return mm.RunMigrations(ctx, configurator, vm) + } +} diff --git a/testing/simapp/upgrades/v6/upgrades.go b/testing/simapp/upgrades/v6/upgrades.go deleted file mode 100644 index 953bccf37e9..00000000000 --- a/testing/simapp/upgrades/v6/upgrades.go +++ /dev/null @@ -1,36 +0,0 @@ -package v6 - -import ( - "github.com/cosmos/cosmos-sdk/codec" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - - v6 "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/migrations/v6" -) - -const ( - // UpgradeName defines the on-chain upgrade name for the SimApp v6 upgrade. - UpgradeName = "v6" -) - -// CreateUpgradeHandler creates an upgrade handler for the v6 SimApp upgrade. -// NOTE: The v6.MigrateICS27ChannelCapabiliity function can be omitted if chains do not yet implement an ICS27 controller module -func CreateUpgradeHandler( - mm *module.Manager, - configurator module.Configurator, - cdc codec.BinaryCodec, - capabilityStoreKey *storetypes.KVStoreKey, - capabilityKeeper *capabilitykeeper.Keeper, - moduleName string, -) upgradetypes.UpgradeHandler { - return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { - if err := v6.MigrateICS27ChannelCapability(ctx, cdc, capabilityStoreKey, capabilityKeeper, moduleName); err != nil { - return nil, err - } - - return mm.RunMigrations(ctx, configurator, vm) - } -} diff --git a/testing/simapp/upgrades/v7/upgrades.go b/testing/simapp/upgrades/v7/upgrades.go deleted file mode 100644 index 4621c06339b..00000000000 --- a/testing/simapp/upgrades/v7/upgrades.go +++ /dev/null @@ -1,42 +0,0 @@ -package v7 - -import ( - "github.com/cosmos/cosmos-sdk/baseapp" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - consensusparamskeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" - paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" - paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - - clientkeeper "github.com/cosmos/ibc-go/v7/modules/core/02-client/keeper" - ibctmmigrations "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint/migrations" -) - -const ( - // UpgradeName defines the on-chain upgrade name for the SimApp v7 upgrade. - UpgradeName = "v7" -) - -// CreateUpgradeHandler creates an upgrade handler for the v7 SimApp upgrade. -func CreateUpgradeHandler( - mm *module.Manager, - configurator module.Configurator, - cdc codec.BinaryCodec, - clientKeeper clientkeeper.Keeper, - consensusParamsKeeper consensusparamskeeper.Keeper, - paramsKeeper paramskeeper.Keeper, -) upgradetypes.UpgradeHandler { - return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { - // OPTIONAL: prune expired tendermint consensus states to save storage space - if _, err := ibctmmigrations.PruneExpiredConsensusStates(ctx, cdc, clientKeeper); err != nil { - return nil, err - } - - legacyBaseAppSubspace := paramsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) - baseapp.MigrateParams(ctx, legacyBaseAppSubspace, &consensusParamsKeeper) - - return mm.RunMigrations(ctx, configurator, vm) - } -}