Skip to content

Commit

Permalink
feat: make 09-localhost stateless (#6683)
Browse files Browse the repository at this point in the history
* Remove client state from localhost light client

* Initial store upgrade for v9 localhost statelessness

* Remove localhost creation and self state proof

* Wire up migrations

* Update docs for 09-localhost for 09-localhost

* lint

* refactor: simplify migrations, remove unnecessary iteration over client state

* refactor: remove localhost client state from queries

* refactor: remove localhost client state, condense localhost impl into single file

* rename: MigrateToStatelessLocalhost

* lint

* Update docs/docs/05-migrations/13-v8-to-v9.md

* Update docs/docs/05-migrations/13-v8-to-v9.md

Co-authored-by: Damian Nolan <damiannolan@gmail.com>

* review: docs + linting cleanups

* refactor: remove unused func

* Update modules/core/02-client/keeper/migrations.go

* markdown lint

---------

Co-authored-by: Carlos Rodriguez <carlos@interchain.io>
Co-authored-by: Colin Axnér <25233464+colin-axner@users.noreply.github.com>
Co-authored-by: Damian Nolan <damiannolan@gmail.com>
  • Loading branch information
4 people committed Jul 3, 2024
1 parent 2bd5920 commit db955c4
Show file tree
Hide file tree
Showing 33 changed files with 232 additions and 1,055 deletions.
42 changes: 26 additions & 16 deletions docs/docs/03-light-clients/02-localhost/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,43 @@ slug: /ibc/light-clients/localhost/overview
Learn about the 09-localhost light client module.
:::

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.
The 09-localhost light client module implements a stateless 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.
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`](03-client-state.md) instance with the client identifier `09-localhost`.
There exists a localhost light client module which can be invoked with the client identifier `09-localhost`. The light
client is stateless, so the `ClientState` is constructed on demand when required.

To supplement this, a [sentinel `ConnectionEnd` is stored in core IBC](04-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.
To supplement this, a [sentinel `ConnectionEnd` is stored in core IBC](04-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](05-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.
[State verification](05-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 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 |
| | 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) | Implicitly made available by the 02-client submodule in core IBC |
| Client updates | Relayer submits headers using `MsgUpdateClient` | No client updates are required as the localhost implementation is stateless |
| 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 |
| `ClientState` storage | `ClientState` stored and directly provable with `VerifyMembership` | Stateless, so `ClientState` is not provable directly with `VerifyMembership` |
60 changes: 1 addition & 59 deletions docs/docs/03-light-clients/02-localhost/03-client-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,6 @@ sidebar_position: 3
slug: /ibc/light-clients/localhost/client-state
---


# `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 {
clientModule, found := k.router.GetRoute(exported.LocalhostClientID)
if !found {
return errorsmod.Wrap(types.ErrRouteNotFound, exported.LocalhostClientID)
}

return clientModule.Initialize(ctx, exported.LocalhostClientID, nil, 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/v8.1.1/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 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.
The 09-localhost client is stateless and has no types.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ sidebar_position: 5
slug: /ibc/light-clients/localhost/state-verification
---


# State verification

The localhost client handles state verification through the `LightClientModule` interface methods `VerifyMembership` and `VerifyNonMembership` by performing read-only operations directly on the core IBC store.
Expand All @@ -20,3 +19,5 @@ The 09-localhost light client module defines a `SentinelProof` as a single byte.
```go
var SentinelProof = []byte{0x01}
```

The `ClientState` of `09-localhost` is stateless, so it is not directly provable with `VerifyMembership` or `VerifyNonMembership`.
8 changes: 8 additions & 0 deletions docs/docs/05-migrations/13-v8-to-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func NewMsgModuleQuerySafe(
- The utility function `QueryLatestConsensusState` of `04-channel` CLI has been removed.
- `UnmarshalPacketData` now takes in the context, portID, and channelID. This allows the packet data to be unmarshaled based on the channel version.
- `Router` reference has been removed from IBC core keeper: [#6138](https://github.com/cosmos/ibc-go/pull/6138). Please use `PortKeeper.Router` instead.
- The function `CreateLocalhostClient` has been removed. The localhost client is now stateless.

### 02-client

Expand Down Expand Up @@ -185,3 +186,10 @@ The `IterateConsensusMetadata` function has been removed.
- The `VerifyMembershipMsg` and `VerifyNonMembershipMsg` payloads for `SudoMsg` have been extended to include a new field, `MerklePath`. The existing `Path` field will remain the same. The new `MerklePath` field is used if and only if the provided key path contains non-utf8 encoded symbols, and as a result will encode the JSON field `merkle_path` as a base64 encoded bytestring. See [23-commitment](#23-commitment).
- The `ExportMetadataMsg` struct has been removed and is no longer required for contracts to implement. Core IBC will handle exporting all key/value's written to the store by a light client contract.
- The `ZeroCustomFields` interface function has been removed from the `ClientState` interface. Core IBC only used this function to set tendermint client states when scheduling an IBC software upgrade. The interface function has been replaced by a type assertion.

### 09-localhost

The `09-localhost` light client has been made stateless and will no longer update the client on every block. The `ClientState` is constructed on demand when required.
The `ClientState` itself is therefore no longer provable direcly with `VerifyMembership` or `VerifyNonMembership`.

Previously stored client state data is pruned automatically on IBC module store migration from `ConsensusVersion` 6 to 7.
4 changes: 4 additions & 0 deletions e2e/tests/core/02-client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@ func (s *ClientTestSuite) TestAllowedClientsParam() {
status, err := query.ClientStatus(ctx, chainA, ibctesting.FirstClientID)
s.Require().NoError(err)
s.Require().Equal(ibcexported.Unauthorized.String(), status)

status, err = query.ClientStatus(ctx, chainA, ibcexported.Localhost)
s.Require().NoError(err)
s.Require().Equal(ibcexported.Unauthorized.String(), status)
})
}

Expand Down
15 changes: 0 additions & 15 deletions e2e/tests/transfer/localhost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,6 @@ func (s *LocalhostTransferTestSuite) TestMsgTransfer_Localhost() {

s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks")

t.Run("verify begin blocker was executed", func(t *testing.T) {
cs, err := query.ClientState(ctx, chainA, exported.LocalhostClientID)
s.Require().NoError(err)

localhostClientState, ok := cs.(*localhost.ClientState)
s.Require().True(ok)
originalHeight := localhostClientState.LatestHeight

s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks")

cs, err = query.ClientState(ctx, chainA, exported.LocalhostClientID)
s.Require().NoError(err)
s.Require().True(cs.(*localhost.ClientState).LatestHeight.GT(originalHeight), "client state height was not incremented")
})

t.Run("channel open init localhost", func(t *testing.T) {
msgChanOpenInit := channeltypes.NewMsgChannelOpenInit(
transfertypes.PortID, channelA.Version,
Expand Down
2 changes: 0 additions & 2 deletions e2e/testsuite/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
solomachine "github.com/cosmos/ibc-go/v8/modules/light-clients/06-solomachine"
ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost"
ibctesting "github.com/cosmos/ibc-go/v8/testing"
simappparams "github.com/cosmos/ibc-go/v8/testing/simapp/params"
)
Expand Down Expand Up @@ -76,7 +75,6 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, simappparams.EncodingConfig) {
channeltypes.RegisterInterfaces(cfg.InterfaceRegistry)
connectiontypes.RegisterInterfaces(cfg.InterfaceRegistry)
ibctmtypes.RegisterInterfaces(cfg.InterfaceRegistry)
localhost.RegisterInterfaces(cfg.InterfaceRegistry)
wasmtypes.RegisterInterfaces(cfg.InterfaceRegistry)

// all other types
Expand Down
8 changes: 0 additions & 8 deletions modules/core/02-client/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/cosmos/ibc-go/v8/modules/core/02-client/keeper"
"github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
"github.com/cosmos/ibc-go/v8/modules/core/exported"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
)

Expand Down Expand Up @@ -33,11 +32,4 @@ 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, exported.LocalhostClientID) == exported.Active {
k.UpdateLocalhostClient(ctx, clientState)
}
}
}
6 changes: 0 additions & 6 deletions modules/core/02-client/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ 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.Errorf("failed to initialise localhost client: %s", err.Error()))
}
}

// ExportGenesis returns the ibc client submodule's exported genesis.
Expand Down
6 changes: 1 addition & 5 deletions modules/core/02-client/keeper/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/cosmos/ibc-go/v8/modules/core/exported"
solomachine "github.com/cosmos/ibc-go/v8/modules/light-clients/06-solomachine"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost"
ibctesting "github.com/cosmos/ibc-go/v8/testing"
)

Expand Down Expand Up @@ -64,10 +63,7 @@ func (suite *KeeperTestSuite) TestCreateClient() {
},
{
"failure: 09-localhost client type not supported",
func() {
lhClientState := localhost.NewClientState(clienttypes.GetSelfHeight(suite.chainA.GetContext()))
clientState = suite.chainA.App.AppCodec().MustMarshal(lhClientState)
},
func() {},
exported.Localhost,
false,
},
Expand Down
20 changes: 2 additions & 18 deletions modules/core/02-client/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,11 @@ func (suite *KeeperTestSuite) TestQueryClientStates() {
{
"empty pagination",
func() {
localhost := types.NewIdentifiedClientState(exported.LocalhostClientID, suite.chainA.GetClientState(exported.LocalhostClientID))
expClientStates = types.IdentifiedClientStates{localhost}
expClientStates = nil
req = &types.QueryClientStatesRequest{}
},
true,
},
{
"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,
CountTotal: true,
},
}
},
true,
},
{
"success",
func() {
Expand All @@ -151,12 +136,11 @@ 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{localhost, idcs, idcs2}.Sort()
expClientStates = types.IdentifiedClientStates{idcs, idcs2}.Sort()
req = &types.QueryClientStatesRequest{
Pagination: &query.PageRequest{
Limit: 20,
Expand Down
10 changes: 0 additions & 10 deletions modules/core/02-client/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,6 @@ func (k *Keeper) Route(clientID string) (exported.LightClientModule, bool) {
return k.router.GetRoute(clientID)
}

// CreateLocalhostClient initialises the 09-localhost client state and sets it in state.
func (k *Keeper) CreateLocalhostClient(ctx sdk.Context) error {
clientModule, found := k.router.GetRoute(exported.LocalhostClientID)
if !found {
return errorsmod.Wrap(types.ErrRouteNotFound, exported.LocalhostClientID)
}

return clientModule.Initialize(ctx, exported.LocalhostClientID, nil, 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 {
clientModule, found := k.router.GetRoute(exported.LocalhostClientID)
Expand Down
Loading

0 comments on commit db955c4

Please sign in to comment.