diff --git a/modules/light-clients/08-wasm/CHANGELOG.md b/modules/light-clients/08-wasm/CHANGELOG.md index 3c4c7ba1e5e..faa9558faa7 100644 --- a/modules/light-clients/08-wasm/CHANGELOG.md +++ b/modules/light-clients/08-wasm/CHANGELOG.md @@ -44,10 +44,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements -* [\#5923](https://github.com/cosmos/ibc-go/pull/5923) imp: adding 08-wasm build opts for libwasmvm linking disabled +* [\#5923](https://github.com/cosmos/ibc-go/pull/5923) imp: add 08-wasm build opts for libwasmvm linking disabled ### Features +* [\#6055](https://github.com/cosmos/ibc-go/pull/6055) feat: add 08-wasm `ConsensusHost` implementation for custom self client/consensus state validation in 03-connection handshake. + ### Bug Fixes diff --git a/modules/light-clients/08-wasm/testing/simapp/app.go b/modules/light-clients/08-wasm/testing/simapp/app.go index d9fbf36d9c1..62632a7c926 100644 --- a/modules/light-clients/08-wasm/testing/simapp/app.go +++ b/modules/light-clients/08-wasm/testing/simapp/app.go @@ -418,6 +418,7 @@ func NewSimApp( app.IBCKeeper = ibckeeper.NewKeeper( appCodec, keys[ibcexported.StoreKey], app.GetSubspace(ibcexported.ModuleName), app.StakingKeeper, app.UpgradeKeeper, scopedIBCKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + // Register the proposal types // Deprecated: Avoid adding new handlers, instead use the new proposal flow // by granting the governance module the right to execute the message. diff --git a/modules/light-clients/08-wasm/types/consensus_host.go b/modules/light-clients/08-wasm/types/consensus_host.go new file mode 100644 index 00000000000..fde71695656 --- /dev/null +++ b/modules/light-clients/08-wasm/types/consensus_host.go @@ -0,0 +1,77 @@ +package types + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +// WasmConsensusHost implements the 02-client types.ConsensusHost interface. +type WasmConsensusHost struct { + cdc codec.BinaryCodec + delegate clienttypes.ConsensusHost +} + +var _ clienttypes.ConsensusHost = (*WasmConsensusHost)(nil) + +// NewWasmConsensusHost creates and returns a new ConsensusHost for wasm wrapped consensus client state and consensus state self validation. +func NewWasmConsensusHost(cdc codec.BinaryCodec, delegate clienttypes.ConsensusHost) (*WasmConsensusHost, error) { + if cdc == nil { + return nil, fmt.Errorf("wasm consensus host codec is nil") + } + + if delegate == nil { + return nil, fmt.Errorf("wasm delegate consensus host is nil") + } + + return &WasmConsensusHost{ + cdc: cdc, + delegate: delegate, + }, nil +} + +// GetSelfConsensusState implements the 02-client types.ConsensusHost interface. +func (w *WasmConsensusHost) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + consensusState, err := w.delegate.GetSelfConsensusState(ctx, height) + if err != nil { + return nil, err + } + + // encode consensusState to wasm.ConsensusState.Data + bz, err := w.cdc.MarshalInterface(consensusState) + if err != nil { + return nil, err + } + + wasmConsensusState := &ConsensusState{ + Data: bz, + } + + return wasmConsensusState, nil +} + +// ValidateSelfClient implements the 02-client types.ConsensusHost interface. +func (w *WasmConsensusHost) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error { + wasmClientState, ok := clientState.(*ClientState) + if !ok { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "client must be a wasm client, expected: %T, got: %T", ClientState{}, wasmClientState) + } + + if wasmClientState.Data == nil { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "wasm client state data is nil") + } + + // unmarshal the wasmClientState bytes into the ClientState interface and call self validation + var unwrappedClientState exported.ClientState + if err := w.cdc.UnmarshalInterface(wasmClientState.Data, &unwrappedClientState); err != nil { + return err + } + + return w.delegate.ValidateSelfClient(ctx, unwrappedClientState) +} diff --git a/modules/light-clients/08-wasm/types/consensus_host_test.go b/modules/light-clients/08-wasm/types/consensus_host_test.go new file mode 100644 index 00000000000..db6ce2db825 --- /dev/null +++ b/modules/light-clients/08-wasm/types/consensus_host_test.go @@ -0,0 +1,151 @@ +package types_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + "github.com/cosmos/ibc-go/v8/testing/mock" +) + +func (suite *TypesTestSuite) TestGetSelfConsensusState() { + var ( + consensusHost clienttypes.ConsensusHost + consensusState exported.ConsensusState + height clienttypes.Height + ) + + cases := []struct { + name string + malleate func() + expError error + }{ + { + name: "success", + malleate: func() {}, + expError: nil, + }, + { + name: "failure: delegate error", + malleate: func() { + consensusHost.(*mock.ConsensusHost).GetSelfConsensusStateFn = func(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + return nil, mock.MockApplicationCallbackError + } + }, + expError: mock.MockApplicationCallbackError, + }, + } + + for i, tc := range cases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + height = clienttypes.ZeroHeight() + + wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientConsensusState) + consensusState = types.NewConsensusState(wrappedClientConsensusStateBz) + + consensusHost = &mock.ConsensusHost{ + GetSelfConsensusStateFn: func(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + return consensusState, nil + }, + } + + tc.malleate() + + var err error + consensusHost, err = types.NewWasmConsensusHost(suite.chainA.Codec, consensusHost) + suite.Require().NoError(err) + + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetConsensusHost( + consensusHost, + ) + + cs, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetSelfConsensusState(suite.chainA.GetContext(), height) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err, "Case %d should have passed: %s", i, tc.name) + suite.Require().NotNil(cs, "Case %d should have passed: %s", i, tc.name) + suite.Require().NotNil(cs.(*types.ConsensusState).Data, "Case %d should have passed: %s", i, tc.name) + } else { + suite.Require().ErrorIs(err, tc.expError, "Case %d should have failed: %s", i, tc.name) + suite.Require().Nil(cs, "Case %d should have failed: %s", i, tc.name) + } + }) + } +} + +func (suite *TypesTestSuite) TestValidateSelfClient() { + var ( + clientState exported.ClientState + consensusHost clienttypes.ConsensusHost + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + name: "success", + malleate: func() {}, + expError: nil, + }, + { + name: "failure: invalid data", + malleate: func() { + clientState = types.NewClientState(nil, wasmtesting.Code, clienttypes.ZeroHeight()) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "failure: invalid clientstate type", + malleate: func() { + clientState = &ibctm.ClientState{} + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "failure: delegate error propagates", + malleate: func() { + consensusHost.(*mock.ConsensusHost).ValidateSelfClientFn = func(ctx sdk.Context, clientState exported.ClientState) error { + return mock.MockApplicationCallbackError + } + }, + expError: mock.MockApplicationCallbackError, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + + clientState = types.NewClientState(wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum), wasmtesting.Code, clienttypes.ZeroHeight()) + consensusHost = &mock.ConsensusHost{} + + tc.malleate() + + var err error + consensusHost, err = types.NewWasmConsensusHost(suite.chainA.Codec, consensusHost) + suite.Require().NoError(err) + + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetConsensusHost( + consensusHost, + ) + + err = suite.chainA.App.GetIBCKeeper().ClientKeeper.ValidateSelfClient(suite.chainA.GetContext(), clientState) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err, "expected valid client for case: %s", tc.name) + } else { + suite.Require().ErrorIs(err, tc.expError, "expected %s got %s", tc.expError, err) + } + }) + } +}