diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e1fb5b56c..8a4ccf5fa01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (core) [\#6055](https://github.com/cosmos/ibc-go/pull/6055) Introduce a new interface `ConsensusHost` used to validate an IBC `ClientState` and `ConsensusState` against the host chain's underlying consensus parameters. * (core/02-client) [\#5821](https://github.com/cosmos/ibc-go/pull/5821) Add rpc `VerifyMembershipProof` (querier approach for conditional clients). * (core/04-channel) [\#5788](https://github.com/cosmos/ibc-go/pull/5788) Add `NewErrorAcknowledgementWithCodespace` to allow codespaces in ack errors. * (apps/27-interchain-accounts) [\#5785](https://github.com/cosmos/ibc-go/pull/5785) Introduce a new tx message that ICA host submodule can use to query the chain (only those marked with `module_query_safe`) and write the responses to the acknowledgement. diff --git a/modules/core/02-client/keeper/keeper.go b/modules/core/02-client/keeper/keeper.go index a635fbef5a7..7db78c5539c 100644 --- a/modules/core/02-client/keeper/keeper.go +++ b/modules/core/02-client/keeper/keeper.go @@ -3,7 +3,6 @@ package keeper import ( "errors" "fmt" - "reflect" "strings" errorsmod "cosmossdk.io/errors" @@ -15,14 +14,9 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cometbft/cometbft/light" - "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" - ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" "github.com/cosmos/ibc-go/v8/modules/core/exported" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost" ) @@ -31,6 +25,7 @@ import ( type Keeper struct { storeKey storetypes.StoreKey cdc codec.BinaryCodec + consensusHost types.ConsensusHost legacySubspace types.ParamSubspace stakingKeeper types.StakingKeeper upgradeKeeper types.UpgradeKeeper @@ -63,6 +58,15 @@ func (k Keeper) UpdateLocalhostClient(ctx sdk.Context, clientState exported.Clie return clientState.UpdateState(ctx, k.cdc, k.ClientStore(ctx, exported.LocalhostClientID), nil) } +// SetConsensusHost sets a custom ConsensusHost for self client state and consensus state validation. +func (k *Keeper) SetConsensusHost(consensusHost types.ConsensusHost) { + if consensusHost == nil { + panic(fmt.Errorf("cannot set a nil self consensus host")) + } + + k.consensusHost = consensusHost +} + // GenerateClientIdentifier returns the next client identifier. func (k Keeper) GenerateClientIdentifier(ctx sdk.Context, clientType string) string { nextClientSeq := k.GetNextClientSequence(ctx) @@ -253,95 +257,15 @@ func (k Keeper) GetLatestClientConsensusState(ctx sdk.Context, clientID string) // and returns the expected consensus state at that height. // For now, can only retrieve self consensus states for the current revision func (k Keeper) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { - selfHeight, ok := height.(types.Height) - if !ok { - return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", types.Height{}, height) - } - // check that height revision matches chainID revision - revision := types.ParseChainID(ctx.ChainID()) - if revision != height.GetRevisionNumber() { - return nil, errorsmod.Wrapf(types.ErrInvalidHeight, "chainID revision number does not match height revision number: expected %d, got %d", revision, height.GetRevisionNumber()) - } - histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, int64(selfHeight.RevisionHeight)) - if err != nil { - return nil, errorsmod.Wrapf(err, "height %d", selfHeight.RevisionHeight) - } - - consensusState := &ibctm.ConsensusState{ - Timestamp: histInfo.Header.Time, - Root: commitmenttypes.NewMerkleRoot(histInfo.Header.GetAppHash()), - NextValidatorsHash: histInfo.Header.NextValidatorsHash, - } - return consensusState, nil + return k.consensusHost.GetSelfConsensusState(ctx, height) } -// ValidateSelfClient validates the client parameters for a client of the running chain -// This function is only used to validate the client state the counterparty stores for this chain -// Client must be in same revision as the executing chain +// ValidateSelfClient validates the client parameters for a client of the running chain. +// This function is only used to validate the client state the counterparty stores for this chain. +// NOTE: If the client type is not of type Tendermint then delegate to a custom client validator function. +// This allows support for non-Tendermint clients, for example 08-wasm clients. func (k Keeper) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error { - tmClient, ok := clientState.(*ibctm.ClientState) - if !ok { - return errorsmod.Wrapf(types.ErrInvalidClient, "client must be a Tendermint client, expected: %T, got: %T", - &ibctm.ClientState{}, tmClient) - } - - if !tmClient.FrozenHeight.IsZero() { - return types.ErrClientFrozen - } - - if ctx.ChainID() != tmClient.ChainId { - return errorsmod.Wrapf(types.ErrInvalidClient, "invalid chain-id. expected: %s, got: %s", - ctx.ChainID(), tmClient.ChainId) - } - - revision := types.ParseChainID(ctx.ChainID()) - - // client must be in the same revision as executing chain - if tmClient.LatestHeight.RevisionNumber != revision { - return errorsmod.Wrapf(types.ErrInvalidClient, "client is not in the same revision as the chain. expected revision: %d, got: %d", - tmClient.LatestHeight.RevisionNumber, revision) - } - - selfHeight := types.NewHeight(revision, uint64(ctx.BlockHeight())) - if tmClient.LatestHeight.GTE(selfHeight) { - return errorsmod.Wrapf(types.ErrInvalidClient, "client has LatestHeight %d greater than or equal to chain height %d", - tmClient.LatestHeight, selfHeight) - } - - expectedProofSpecs := commitmenttypes.GetSDKSpecs() - if !reflect.DeepEqual(expectedProofSpecs, tmClient.ProofSpecs) { - return errorsmod.Wrapf(types.ErrInvalidClient, "client has invalid proof specs. expected: %v got: %v", - expectedProofSpecs, tmClient.ProofSpecs) - } - - if err := light.ValidateTrustLevel(tmClient.TrustLevel.ToTendermint()); err != nil { - return errorsmod.Wrapf(types.ErrInvalidClient, "trust-level invalid: %v", err) - } - - expectedUbdPeriod, err := k.stakingKeeper.UnbondingTime(ctx) - if err != nil { - return errorsmod.Wrapf(err, "failed to retrieve unbonding period") - } - - if expectedUbdPeriod != tmClient.UnbondingPeriod { - return errorsmod.Wrapf(types.ErrInvalidClient, "invalid unbonding period. expected: %s, got: %s", - expectedUbdPeriod, tmClient.UnbondingPeriod) - } - - if tmClient.UnbondingPeriod < tmClient.TrustingPeriod { - return errorsmod.Wrapf(types.ErrInvalidClient, "unbonding period must be greater than trusting period. unbonding period (%d) < trusting period (%d)", - tmClient.UnbondingPeriod, tmClient.TrustingPeriod) - } - - if len(tmClient.UpgradePath) != 0 { - // For now, SDK IBC implementation assumes that upgrade path (if defined) is defined by SDK upgrade module - expectedUpgradePath := []string{upgradetypes.StoreKey, upgradetypes.KeyUpgradedIBCState} - if !reflect.DeepEqual(expectedUpgradePath, tmClient.UpgradePath) { - return errorsmod.Wrapf(types.ErrInvalidClient, "upgrade path must be the upgrade path defined by upgrade module. expected %v, got %v", - expectedUpgradePath, tmClient.UpgradePath) - } - } - return nil + return k.consensusHost.ValidateSelfClient(ctx, clientState) } // GetUpgradePlan executes the upgrade keeper GetUpgradePlan function. diff --git a/modules/core/02-client/keeper/keeper_test.go b/modules/core/02-client/keeper/keeper_test.go index 1a73391bd4d..94bc21b0372 100644 --- a/modules/core/02-client/keeper/keeper_test.go +++ b/modules/core/02-client/keeper/keeper_test.go @@ -23,7 +23,6 @@ import ( "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" "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" @@ -44,10 +43,7 @@ const ( maxClockDrift time.Duration = time.Second * 10 ) -var ( - testClientHeight = types.NewHeight(0, 5) - testClientHeightRevision1 = types.NewHeight(1, 5) -) +var testClientHeight = types.NewHeight(0, 5) type KeeperTestSuite struct { testifysuite.Suite @@ -144,91 +140,7 @@ func (suite *KeeperTestSuite) TestSetClientConsensusState() { suite.Require().Equal(suite.consensusState, tmConsState, "ConsensusState not stored correctly") } -func (suite *KeeperTestSuite) TestValidateSelfClient() { - testClientHeight := types.GetSelfHeight(suite.chainA.GetContext()) - testClientHeight.RevisionHeight-- - - testCases := []struct { - name string - clientState exported.ClientState - expPass bool - }{ - { - "success", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - true, - }, - { - "success with nil UpgradePath", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), nil), - true, - }, - { - "frozen client", - &ibctm.ClientState{ChainId: suite.chainA.ChainID, TrustLevel: ibctm.DefaultTrustLevel, TrustingPeriod: trustingPeriod, UnbondingPeriod: ubdPeriod, MaxClockDrift: maxClockDrift, FrozenHeight: testClientHeight, LatestHeight: testClientHeight, ProofSpecs: commitmenttypes.GetSDKSpecs(), UpgradePath: ibctesting.UpgradePath}, - false, - }, - { - "incorrect chainID", - ibctm.NewClientState("gaiatestnet", ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - false, - }, - { - "invalid client height", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, types.GetSelfHeight(suite.chainA.GetContext()).Increment().(types.Height), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - false, - }, - { - "invalid client type", - solomachine.NewClientState(0, &solomachine.ConsensusState{PublicKey: suite.solomachine.ConsensusState().PublicKey, Diversifier: suite.solomachine.Diversifier, Timestamp: suite.solomachine.Time}), - false, - }, - { - "invalid client revision", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeightRevision1, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - false, - }, - { - "invalid proof specs", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, nil, ibctesting.UpgradePath), - false, - }, - { - "invalid trust level", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.Fraction{Numerator: 0, Denominator: 1}, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), false, - }, - { - "invalid unbonding period", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod+10, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - false, - }, - { - "invalid trusting period", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, ubdPeriod+10, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), - false, - }, - { - "invalid upgrade path", - ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), []string{"bad", "upgrade", "path"}), - false, - }, - } - - for _, tc := range testCases { - tc := tc - - suite.Run(tc.name, func() { - err := suite.chainA.App.GetIBCKeeper().ClientKeeper.ValidateSelfClient(suite.chainA.GetContext(), tc.clientState) - if tc.expPass { - suite.Require().NoError(err, "expected valid client for case: %s", tc.name) - } else { - suite.Require().Error(err, "expected invalid client for case: %s", tc.name) - } - }) - } -} - -func (suite KeeperTestSuite) TestGetAllGenesisClients() { //nolint:govet // this is a test, we are okay with copying locks +func (suite *KeeperTestSuite) TestGetAllGenesisClients() { clientIDs := []string{ exported.LocalhostClientID, testClientID2, testClientID3, testClientID, } @@ -251,7 +163,7 @@ func (suite KeeperTestSuite) TestGetAllGenesisClients() { //nolint:govet // this suite.Require().Equal(expGenClients.Sort(), genClients) } -func (suite KeeperTestSuite) TestGetAllGenesisMetadata() { //nolint:govet // this is a test, we are okay with copying locks +func (suite *KeeperTestSuite) TestGetAllGenesisMetadata() { //nolint:govet // this is a test, we are okay with copying locks expectedGenMetadata := []types.IdentifiedGenesisMetadata{ types.NewIdentifiedGenesisMetadata( "07-tendermint-1", @@ -281,35 +193,9 @@ func (suite KeeperTestSuite) TestGetAllGenesisMetadata() { //nolint:govet // thi suite.Require().Equal(expectedGenMetadata, actualGenMetadata, "retrieved metadata is unexpected") } -func (suite KeeperTestSuite) TestGetConsensusState() { //nolint:govet // this is a test, we are okay with copying locks - suite.ctx = suite.ctx.WithBlockHeight(10) - cases := []struct { - name string - height types.Height - expPass bool - }{ - {"zero height", types.ZeroHeight(), false}, - {"height > latest height", types.NewHeight(0, uint64(suite.ctx.BlockHeight())+1), false}, - {"latest height - 1", types.NewHeight(0, uint64(suite.ctx.BlockHeight())-1), true}, - {"latest height", types.GetSelfHeight(suite.ctx), true}, - } - - for i, tc := range cases { - tc := tc - cs, err := suite.keeper.GetSelfConsensusState(suite.ctx, tc.height) - if tc.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) - } else { - suite.Require().Error(err, "Case %d should have failed: %s", i, tc.name) - suite.Require().Nil(cs, "Case %d should have failed: %s", i, tc.name) - } - } -} - // 2 clients in total are created on chainA. The first client is updated so it contains an initial consensus state // and a consensus state at the update height. -func (suite KeeperTestSuite) TestGetAllConsensusStates() { //nolint:govet // this is a test, we are okay with copying locks +func (suite *KeeperTestSuite) TestGetAllConsensusStates() { //nolint:govet // this is a test, we are okay with copying locks path := ibctesting.NewPath(suite.chainA, suite.chainB) suite.coordinator.SetupClients(path) @@ -358,7 +244,7 @@ func (suite KeeperTestSuite) TestGetAllConsensusStates() { //nolint:govet // thi suite.Require().Equal(expConsensusStates, consStates, "%s \n\n%s", expConsensusStates, consStates) } -func (suite KeeperTestSuite) TestIterateClientStates() { //nolint:govet // this is a test, we are okay with copying locks +func (suite *KeeperTestSuite) TestIterateClientStates() { //nolint:govet // this is a test, we are okay with copying locks paths := []*ibctesting.Path{ ibctesting.NewPath(suite.chainA, suite.chainB), ibctesting.NewPath(suite.chainA, suite.chainB), diff --git a/modules/core/02-client/types/client.go b/modules/core/02-client/types/client.go index 31da1a54e70..2b1cb965b85 100644 --- a/modules/core/02-client/types/client.go +++ b/modules/core/02-client/types/client.go @@ -11,6 +11,7 @@ import ( errorsmod "cosmossdk.io/errors" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" "github.com/cosmos/ibc-go/v8/modules/core/exported" @@ -21,6 +22,12 @@ var ( _ codectypes.UnpackInterfacesMessage = (*ConsensusStateWithHeight)(nil) ) +// ConsensusHost defines an interface used to validate an IBC ClientState and ConsensusState against the host chain's underlying consensus parameters. +type ConsensusHost interface { + GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) + ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error +} + // NewIdentifiedClientState creates a new IdentifiedClientState instance func NewIdentifiedClientState(clientID string, clientState exported.ClientState) IdentifiedClientState { msg, ok := clientState.(proto.Message) diff --git a/modules/core/02-client/types/errors.go b/modules/core/02-client/types/errors.go index 3a726378cbc..3dc556901f8 100644 --- a/modules/core/02-client/types/errors.go +++ b/modules/core/02-client/types/errors.go @@ -36,4 +36,5 @@ var ( 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") + ErrClientTypeNotSupported = errorsmod.Register(SubModuleName, 32, "client type not supported") ) diff --git a/modules/core/03-connection/keeper/handshake.go b/modules/core/03-connection/keeper/handshake.go index c38d0504edd..95e9947ae31 100644 --- a/modules/core/03-connection/keeper/handshake.go +++ b/modules/core/03-connection/keeper/handshake.go @@ -94,11 +94,11 @@ func (k Keeper) ConnOpenTry( } // validate client parameters of a chainB client stored on chainA - if err := k.clientKeeper.ValidateSelfClient(ctx, clientState); err != nil { + if err := k.consensusHost.ValidateSelfClient(ctx, clientState); err != nil { return "", err } - expectedConsensusState, err := k.clientKeeper.GetSelfConsensusState(ctx, consensusHeight) + expectedConsensusState, err := k.consensusHost.GetSelfConsensusState(ctx, consensusHeight) if err != nil { return "", errorsmod.Wrapf(err, "self consensus state not found for height %s", consensusHeight.String()) } @@ -205,12 +205,12 @@ func (k Keeper) ConnOpenAck( } // validate client parameters of a chainA client stored on chainB - if err := k.clientKeeper.ValidateSelfClient(ctx, clientState); err != nil { + if err := k.consensusHost.ValidateSelfClient(ctx, clientState); err != nil { return err } // Retrieve chainA's consensus state at consensusheight - expectedConsensusState, err := k.clientKeeper.GetSelfConsensusState(ctx, consensusHeight) + expectedConsensusState, err := k.consensusHost.GetSelfConsensusState(ctx, consensusHeight) if err != nil { return errorsmod.Wrapf(err, "self consensus state not found for height %s", consensusHeight.String()) } diff --git a/modules/core/03-connection/keeper/handshake_test.go b/modules/core/03-connection/keeper/handshake_test.go index c60361aaccc..dba6f03e448 100644 --- a/modules/core/03-connection/keeper/handshake_test.go +++ b/modules/core/03-connection/keeper/handshake_test.go @@ -3,12 +3,15 @@ package keeper_test import ( "time" + 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/03-connection/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" "github.com/cosmos/ibc-go/v8/modules/core/exported" ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v8/testing" + "github.com/cosmos/ibc-go/v8/testing/mock" ) // TestConnOpenInit - chainA initializes (INIT state) a connection with @@ -218,6 +221,21 @@ func (suite *KeeperTestSuite) TestConnOpenTry() { err := path.EndpointA.ConnOpenInit() suite.Require().NoError(err) }, false}, + {"override self consensus host", func() { + err := path.EndpointA.ConnOpenInit() + suite.Require().NoError(err) + + // retrieve client state of chainA to pass as counterpartyClient + counterpartyClient = suite.chainA.GetClientState(path.EndpointA.ClientID) + + mockValidator := mock.ConsensusHost{ + ValidateSelfClientFn: func(ctx sdk.Context, clientState exported.ClientState) error { + return mock.MockApplicationCallbackError + }, + } + + suite.chainB.App.GetIBCKeeper().SetConsensusHost(&mockValidator) + }, false}, } for _, tc := range testCases { diff --git a/modules/core/03-connection/keeper/keeper.go b/modules/core/03-connection/keeper/keeper.go index cd18555f56e..6bb3af463dc 100644 --- a/modules/core/03-connection/keeper/keeper.go +++ b/modules/core/03-connection/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "errors" + "fmt" errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" @@ -26,6 +27,7 @@ type Keeper struct { legacySubspace types.ParamSubspace cdc codec.BinaryCodec clientKeeper types.ClientKeeper + consensusHost clienttypes.ConsensusHost } // NewKeeper creates a new IBC connection Keeper instance @@ -43,6 +45,15 @@ func (Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+exported.ModuleName+"/"+types.SubModuleName) } +// SetConsensusHost sets a custom ConsensusHost for self client state and consensus state validation. +func (k *Keeper) SetConsensusHost(consensusHost clienttypes.ConsensusHost) { + if consensusHost == nil { + panic(fmt.Errorf("cannot set a nil self consensus host")) + } + + k.consensusHost = consensusHost +} + // GetCommitmentPrefix returns the IBC connection store prefix as a commitment // Prefix func (k Keeper) GetCommitmentPrefix() exported.Prefix { diff --git a/modules/core/keeper/keeper.go b/modules/core/keeper/keeper.go index 08ee5dc027e..d8df3b6b96a 100644 --- a/modules/core/keeper/keeper.go +++ b/modules/core/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "errors" + "fmt" "reflect" "strings" @@ -17,6 +18,7 @@ import ( portkeeper "github.com/cosmos/ibc-go/v8/modules/core/05-port/keeper" porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" "github.com/cosmos/ibc-go/v8/modules/core/types" + ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" ) var _ types.QueryServer = (*Keeper)(nil) @@ -64,7 +66,7 @@ func NewKeeper( portKeeper := portkeeper.NewKeeper(scopedKeeper) channelKeeper := channelkeeper.NewKeeper(cdc, key, clientKeeper, connectionKeeper, &portKeeper, scopedKeeper) - return &Keeper{ + keeper := &Keeper{ cdc: cdc, ClientKeeper: clientKeeper, ConnectionKeeper: connectionKeeper, @@ -72,6 +74,10 @@ func NewKeeper( PortKeeper: &portKeeper, authority: authority, } + + keeper.SetConsensusHost(ibctm.NewConsensusHost(stakingKeeper)) + + return keeper } // Codec returns the IBC module codec. @@ -79,6 +85,16 @@ func (k Keeper) Codec() codec.BinaryCodec { return k.cdc } +// SetConsensusHost sets a custom ConsensusHost for self client state and consensus state validation. +func (k *Keeper) SetConsensusHost(consensusHost clienttypes.ConsensusHost) { + if consensusHost == nil { + panic(fmt.Errorf("cannot set a nil self consensus host")) + } + + k.ClientKeeper.SetConsensusHost(consensusHost) + k.ConnectionKeeper.SetConsensusHost(consensusHost) +} + // SetRouter sets the Router in IBC Keeper and seals it. The method panics if // there is an existing router that's already sealed. func (k *Keeper) SetRouter(rtr *porttypes.Router) { diff --git a/modules/core/keeper/migrations.go b/modules/core/keeper/migrations.go deleted file mode 100644 index 9afaddcb3c7..00000000000 --- a/modules/core/keeper/migrations.go +++ /dev/null @@ -1,23 +0,0 @@ -package keeper - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - - clientkeeper "github.com/cosmos/ibc-go/v8/modules/core/02-client/keeper" -) - -// 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} -} - -// Migrate2to3 migrates from version 2 to 3. See 02-client keeper function Migrate2to3. -func (m Migrator) Migrate2to3(ctx sdk.Context) error { - clientMigrator := clientkeeper.NewMigrator(m.keeper.ClientKeeper) - return clientMigrator.Migrate2to3(ctx) -} diff --git a/modules/light-clients/07-tendermint/consensus_host.go b/modules/light-clients/07-tendermint/consensus_host.go new file mode 100644 index 00000000000..0ce7bb4e802 --- /dev/null +++ b/modules/light-clients/07-tendermint/consensus_host.go @@ -0,0 +1,134 @@ +package tendermint + +import ( + "context" + "reflect" + "time" + + errorsmod "cosmossdk.io/errors" + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/cometbft/cometbft/light" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" + "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +var _ clienttypes.ConsensusHost = (*ConsensusHost)(nil) + +// ConsensusHost implements the 02-client clienttypes.ConsensusHost interface. +type ConsensusHost struct { + stakingKeeper StakingKeeper +} + +// StakingKeeper defines an expected interface for the tendermint ConsensusHost. +type StakingKeeper interface { + GetHistoricalInfo(ctx context.Context, height int64) (stakingtypes.HistoricalInfo, error) + UnbondingTime(ctx context.Context) (time.Duration, error) +} + +// NewConsensusHost creates and returns a new ConsensusHost for tendermint consensus. +func NewConsensusHost(stakingKeeper clienttypes.StakingKeeper) clienttypes.ConsensusHost { + return &ConsensusHost{ + stakingKeeper: stakingKeeper, + } +} + +// GetSelfConsensusState implements the 02-client clienttypes.ConsensusHost interface. +func (c *ConsensusHost) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + selfHeight, ok := height.(clienttypes.Height) + if !ok { + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) + } + + // check that height revision matches chainID revision + revision := clienttypes.ParseChainID(ctx.ChainID()) + if revision != height.GetRevisionNumber() { + return nil, errorsmod.Wrapf(clienttypes.ErrInvalidHeight, "chainID revision number does not match height revision number: expected %d, got %d", revision, height.GetRevisionNumber()) + } + + histInfo, err := c.stakingKeeper.GetHistoricalInfo(ctx, int64(selfHeight.RevisionHeight)) + if err != nil { + return nil, errorsmod.Wrapf(err, "height %d", selfHeight.RevisionHeight) + } + + consensusState := &ConsensusState{ + Timestamp: histInfo.Header.Time, + Root: commitmenttypes.NewMerkleRoot(histInfo.Header.GetAppHash()), + NextValidatorsHash: histInfo.Header.NextValidatorsHash, + } + + return consensusState, nil +} + +// ValidateSelfClient implements the 02-client clienttypes.ConsensusHost interface. +func (c *ConsensusHost) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error { + tmClient, ok := clientState.(*ClientState) + if !ok { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "client must be a Tendermint client, expected: %T, got: %T", &ClientState{}, tmClient) + } + + if !tmClient.FrozenHeight.IsZero() { + return clienttypes.ErrClientFrozen + } + + if ctx.ChainID() != tmClient.ChainId { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "invalid chain-id. expected: %s, got: %s", + ctx.ChainID(), tmClient.ChainId) + } + + revision := clienttypes.ParseChainID(ctx.ChainID()) + + // client must be in the same revision as executing chain + if tmClient.LatestHeight.RevisionNumber != revision { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "client is not in the same revision as the chain. expected revision: %d, got: %d", + tmClient.LatestHeight.RevisionNumber, revision) + } + + selfHeight := clienttypes.NewHeight(revision, uint64(ctx.BlockHeight())) + if tmClient.LatestHeight.GTE(selfHeight) { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "client has LatestHeight %d greater than or equal to chain height %d", + tmClient.LatestHeight, selfHeight) + } + + expectedProofSpecs := commitmenttypes.GetSDKSpecs() + if !reflect.DeepEqual(expectedProofSpecs, tmClient.ProofSpecs) { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "client has invalid proof specs. expected: %v got: %v", + expectedProofSpecs, tmClient.ProofSpecs) + } + + if err := light.ValidateTrustLevel(tmClient.TrustLevel.ToTendermint()); err != nil { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "trust-level invalid: %v", err) + } + + expectedUbdPeriod, err := c.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return errorsmod.Wrapf(err, "failed to retrieve unbonding period") + } + + if expectedUbdPeriod != tmClient.UnbondingPeriod { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "invalid unbonding period. expected: %s, got: %s", + expectedUbdPeriod, tmClient.UnbondingPeriod) + } + + if tmClient.UnbondingPeriod < tmClient.TrustingPeriod { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "unbonding period must be greater than trusting period. unbonding period (%d) < trusting period (%d)", + tmClient.UnbondingPeriod, tmClient.TrustingPeriod) + } + + if len(tmClient.UpgradePath) != 0 { + // For now, SDK IBC implementation assumes that upgrade path (if defined) is defined by SDK upgrade module + expectedUpgradePath := []string{upgradetypes.StoreKey, upgradetypes.KeyUpgradedIBCState} + if !reflect.DeepEqual(expectedUpgradePath, tmClient.UpgradePath) { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "upgrade path must be the upgrade path defined by upgrade module. expected %v, got %v", + expectedUpgradePath, tmClient.UpgradePath) + } + } + + return nil +} diff --git a/modules/light-clients/07-tendermint/consensus_host_test.go b/modules/light-clients/07-tendermint/consensus_host_test.go new file mode 100644 index 00000000000..bb2f03d5b1b --- /dev/null +++ b/modules/light-clients/07-tendermint/consensus_host_test.go @@ -0,0 +1,249 @@ +package tendermint_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + "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" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + "github.com/cosmos/ibc-go/v8/testing/mock" +) + +func (suite *TendermintTestSuite) TestGetSelfConsensusState() { + var height clienttypes.Height + + cases := []struct { + name string + malleate func() + expError error + }{ + { + name: "zero height", + malleate: func() {}, + expError: clienttypes.ErrInvalidHeight, + }, + { + name: "height > latest height", + malleate: func() { + height = clienttypes.NewHeight(1, uint64(suite.chainA.GetContext().BlockHeight())+1) + }, + expError: stakingtypes.ErrNoHistoricalInfo, + }, + { + name: "pruned historical info", + malleate: func() { + height = clienttypes.NewHeight(1, uint64(suite.chainA.GetContext().BlockHeight())-1) + + err := suite.chainA.GetSimApp().StakingKeeper.DeleteHistoricalInfo(suite.chainA.GetContext(), int64(height.GetRevisionHeight())) + suite.Require().NoError(err) + }, + expError: stakingtypes.ErrNoHistoricalInfo, + }, + { + name: "custom consensus host: failure", + malleate: func() { + consensusHost := &mock.ConsensusHost{ + GetSelfConsensusStateFn: func(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + return nil, mock.MockApplicationCallbackError + }, + } + suite.chainA.GetSimApp().GetIBCKeeper().SetConsensusHost(consensusHost) + }, + expError: mock.MockApplicationCallbackError, + }, + { + name: "custom consensus host: success", + malleate: func() { + consensusHost := &mock.ConsensusHost{ + GetSelfConsensusStateFn: func(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + return &solomachine.ConsensusState{}, nil + }, + } + suite.chainA.GetSimApp().GetIBCKeeper().SetConsensusHost(consensusHost) + }, + expError: nil, + }, + { + name: "latest height - 1", + malleate: func() { + height = clienttypes.NewHeight(1, uint64(suite.chainA.GetContext().BlockHeight())-1) + }, + expError: nil, + }, + { + name: "latest height", + malleate: func() { + // historical info is set on BeginBlock in x/staking, which is now encapsulated within the FinalizeBlock abci method, + // thus, we do not have historical info for current height due to how the ibctesting library operates. + // ibctesting calls app.Commit() as a final step on NextBlock and we invoke test code before FinalizeBlock is called at the current height once again. + err := suite.chainA.GetSimApp().StakingKeeper.TrackHistoricalInfo(suite.chainA.GetContext()) + suite.Require().NoError(err) + + height = clienttypes.GetSelfHeight(suite.chainA.GetContext()) + }, + expError: nil, + }, + } + + for i, tc := range cases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + + height = clienttypes.ZeroHeight() + + tc.malleate() + + cs, err := suite.chainA.GetSimApp().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) + } 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 *TendermintTestSuite) TestValidateSelfClient() { + testClientHeight := clienttypes.GetSelfHeight(suite.chainA.GetContext()) + testClientHeight.RevisionHeight-- + + var clientState exported.ClientState + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + name: "success", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: nil, + }, + { + name: "success with nil UpgradePath", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), nil) + }, + expError: nil, + }, + { + name: "success with custom self validator: solomachine", + malleate: func() { + clientState = solomachine.NewClientState(1, &solomachine.ConsensusState{}) + + smConsensusHost := &mock.ConsensusHost{ + ValidateSelfClientFn: func(ctx sdk.Context, clientState exported.ClientState) error { + smClientState, ok := clientState.(*solomachine.ClientState) + suite.Require().True(ok) + suite.Require().Equal(uint64(1), smClientState.Sequence) + + return nil + }, + } + + // add mock validation logic + suite.chainA.App.GetIBCKeeper().SetConsensusHost(smConsensusHost) + }, + expError: nil, + }, + { + name: "frozen client", + malleate: func() { + clientState = &ibctm.ClientState{ChainId: suite.chainA.ChainID, TrustLevel: ibctm.DefaultTrustLevel, TrustingPeriod: trustingPeriod, UnbondingPeriod: ubdPeriod, MaxClockDrift: maxClockDrift, FrozenHeight: testClientHeight, LatestHeight: testClientHeight, ProofSpecs: commitmenttypes.GetSDKSpecs(), UpgradePath: ibctesting.UpgradePath} + }, + expError: clienttypes.ErrClientFrozen, + }, + { + name: "incorrect chainID", + malleate: func() { + clientState = ibctm.NewClientState("gaiatestnet", ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid client height", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, clienttypes.GetSelfHeight(suite.chainA.GetContext()).Increment().(clienttypes.Height), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid client type", + malleate: func() { + clientState = solomachine.NewClientState(0, &solomachine.ConsensusState{}) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid client revision", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, clienttypes.NewHeight(1, 5), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid proof specs", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, nil, ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid trust level", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.Fraction{Numerator: 0, Denominator: 1}, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid unbonding period", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod+10, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid trusting period", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, ubdPeriod+10, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath) + }, + expError: clienttypes.ErrInvalidClient, + }, + { + name: "invalid upgrade path", + malleate: func() { + clientState = ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), []string{"bad", "upgrade", "path"}) + }, + expError: clienttypes.ErrInvalidClient, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + + tc.malleate() + + 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().Error(err, "expected invalid client for case: %s", tc.name) + } + }) + } +} diff --git a/testing/mock/consensus_host.go b/testing/mock/consensus_host.go new file mode 100644 index 00000000000..4ce25373a53 --- /dev/null +++ b/testing/mock/consensus_host.go @@ -0,0 +1,31 @@ +package mock + +import ( + 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" +) + +var _ clienttypes.ConsensusHost = (*ConsensusHost)(nil) + +type ConsensusHost struct { + GetSelfConsensusStateFn func(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) + ValidateSelfClientFn func(ctx sdk.Context, clientState exported.ClientState) error +} + +func (cv *ConsensusHost) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) { + if cv.GetSelfConsensusStateFn == nil { + return nil, nil + } + + return cv.GetSelfConsensusStateFn(ctx, height) +} + +func (cv *ConsensusHost) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error { + if cv.ValidateSelfClientFn == nil { + return nil + } + + return cv.ValidateSelfClientFn(ctx, clientState) +}