Skip to content

Commit 50d2a08

Browse files
damiannolanchatton
andauthored
feat: adding ConsensusHost interface for custom self client/consensus state validation (#6055)
Co-authored-by: chatton <[email protected]>
1 parent bb739af commit 50d2a08

File tree

23 files changed

+710
-254
lines changed

23 files changed

+710
-254
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
5353
### Features
5454

5555
* (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.
56+
* (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.
5657

5758
### Bug Fixes
5859

modules/core/02-client/abci.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// BeginBlocker is used to perform IBC client upgrades
12-
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
12+
func BeginBlocker(ctx sdk.Context, k *keeper.Keeper) {
1313
plan, err := k.GetUpgradePlan(ctx)
1414
if err == nil {
1515
// Once we are at the last block this chain will commit, set the upgraded consensus state

modules/core/02-client/keeper/keeper.go

+20-97
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package keeper
33
import (
44
"errors"
55
"fmt"
6-
"reflect"
76
"strings"
87

98
errorsmod "cosmossdk.io/errors"
@@ -15,12 +14,8 @@ import (
1514
"github.com/cosmos/cosmos-sdk/codec"
1615
sdk "github.com/cosmos/cosmos-sdk/types"
1716

18-
"github.com/cometbft/cometbft/light"
19-
2017
"github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
21-
commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"
2218
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
23-
ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors"
2419
"github.com/cosmos/ibc-go/v8/modules/core/exported"
2520
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
2621
localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost"
@@ -32,8 +27,8 @@ type Keeper struct {
3227
storeKey storetypes.StoreKey
3328
cdc codec.BinaryCodec
3429
router *types.Router
30+
consensusHost types.ConsensusHost
3531
legacySubspace types.ParamSubspace
36-
stakingKeeper types.StakingKeeper
3732
upgradeKeeper types.UpgradeKeeper
3833
}
3934

@@ -47,8 +42,8 @@ func NewKeeper(cdc codec.BinaryCodec, key storetypes.StoreKey, legacySubspace ty
4742
storeKey: key,
4843
cdc: cdc,
4944
router: router,
45+
consensusHost: ibctm.NewConsensusHost(sk),
5046
legacySubspace: legacySubspace,
51-
stakingKeeper: sk,
5247
upgradeKeeper: uk,
5348
}
5449
}
@@ -88,6 +83,15 @@ func (k Keeper) UpdateLocalhostClient(ctx sdk.Context, clientState exported.Clie
8883
return clientModule.UpdateState(ctx, exported.LocalhostClientID, nil)
8984
}
9085

86+
// SetSelfConsensusHost sets a custom ConsensusHost for self client state and consensus state validation.
87+
func (k *Keeper) SetSelfConsensusHost(consensusHost types.ConsensusHost) {
88+
if consensusHost == nil {
89+
panic(fmt.Errorf("cannot set a nil self consensus host"))
90+
}
91+
92+
k.consensusHost = consensusHost
93+
}
94+
9195
// GenerateClientIdentifier returns the next client identifier.
9296
func (k Keeper) GenerateClientIdentifier(ctx sdk.Context, clientType string) string {
9397
nextClientSeq := k.GetNextClientSequence(ctx)
@@ -99,7 +103,7 @@ func (k Keeper) GenerateClientIdentifier(ctx sdk.Context, clientType string) str
99103
}
100104

101105
// GetClientState gets a particular client from the store
102-
func (k Keeper) GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) {
106+
func (k *Keeper) GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) {
103107
store := k.ClientStore(ctx, clientID)
104108
bz := store.Get(host.ClientStateKey())
105109
if len(bz) == 0 {
@@ -111,13 +115,13 @@ func (k Keeper) GetClientState(ctx sdk.Context, clientID string) (exported.Clien
111115
}
112116

113117
// SetClientState sets a particular Client to the store
114-
func (k Keeper) SetClientState(ctx sdk.Context, clientID string, clientState exported.ClientState) {
118+
func (k *Keeper) SetClientState(ctx sdk.Context, clientID string, clientState exported.ClientState) {
115119
store := k.ClientStore(ctx, clientID)
116120
store.Set(host.ClientStateKey(), k.MustMarshalClientState(clientState))
117121
}
118122

119123
// GetClientConsensusState gets the stored consensus state from a client at a given height.
120-
func (k Keeper) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) {
124+
func (k *Keeper) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) {
121125
store := k.ClientStore(ctx, clientID)
122126
bz := store.Get(host.ConsensusStateKey(height))
123127
if len(bz) == 0 {
@@ -308,96 +312,15 @@ func (k Keeper) GetLatestClientConsensusState(ctx sdk.Context, clientID string)
308312
// and returns the expected consensus state at that height.
309313
// For now, can only retrieve self consensus states for the current revision
310314
func (k Keeper) GetSelfConsensusState(ctx sdk.Context, height exported.Height) (exported.ConsensusState, error) {
311-
selfHeight, ok := height.(types.Height)
312-
if !ok {
313-
return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", types.Height{}, height)
314-
}
315-
// check that height revision matches chainID revision
316-
revision := types.ParseChainID(ctx.ChainID())
317-
if revision != height.GetRevisionNumber() {
318-
return nil, errorsmod.Wrapf(types.ErrInvalidHeight, "chainID revision number does not match height revision number: expected %d, got %d", revision, height.GetRevisionNumber())
319-
}
320-
histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, int64(selfHeight.RevisionHeight))
321-
if err != nil {
322-
return nil, errorsmod.Wrapf(err, "height %d", selfHeight.RevisionHeight)
323-
}
324-
325-
consensusState := &ibctm.ConsensusState{
326-
Timestamp: histInfo.Header.Time,
327-
Root: commitmenttypes.NewMerkleRoot(histInfo.Header.GetAppHash()),
328-
NextValidatorsHash: histInfo.Header.NextValidatorsHash,
329-
}
330-
331-
return consensusState, nil
315+
return k.consensusHost.GetSelfConsensusState(ctx, height)
332316
}
333317

334-
// ValidateSelfClient validates the client parameters for a client of the running chain
335-
// This function is only used to validate the client state the counterparty stores for this chain
336-
// Client must be in same revision as the executing chain
318+
// ValidateSelfClient validates the client parameters for a client of the running chain.
319+
// This function is only used to validate the client state the counterparty stores for this chain.
320+
// NOTE: If the client type is not of type Tendermint then delegate to a custom client validator function.
321+
// This allows support for non-Tendermint clients, for example 08-wasm clients.
337322
func (k Keeper) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error {
338-
tmClient, ok := clientState.(*ibctm.ClientState)
339-
if !ok {
340-
return errorsmod.Wrapf(types.ErrInvalidClient, "client must be a Tendermint client, expected: %T, got: %T",
341-
&ibctm.ClientState{}, tmClient)
342-
}
343-
344-
if !tmClient.FrozenHeight.IsZero() {
345-
return types.ErrClientFrozen
346-
}
347-
348-
if ctx.ChainID() != tmClient.ChainId {
349-
return errorsmod.Wrapf(types.ErrInvalidClient, "invalid chain-id. expected: %s, got: %s",
350-
ctx.ChainID(), tmClient.ChainId)
351-
}
352-
353-
revision := types.ParseChainID(ctx.ChainID())
354-
355-
// client must be in the same revision as executing chain
356-
if tmClient.LatestHeight.RevisionNumber != revision {
357-
return errorsmod.Wrapf(types.ErrInvalidClient, "client is not in the same revision as the chain. expected revision: %d, got: %d",
358-
tmClient.LatestHeight.RevisionNumber, revision)
359-
}
360-
361-
selfHeight := types.NewHeight(revision, uint64(ctx.BlockHeight()))
362-
if tmClient.LatestHeight.GTE(selfHeight) {
363-
return errorsmod.Wrapf(types.ErrInvalidClient, "client has LatestHeight %d greater than or equal to chain height %d",
364-
tmClient.LatestHeight, selfHeight)
365-
}
366-
367-
expectedProofSpecs := commitmenttypes.GetSDKSpecs()
368-
if !reflect.DeepEqual(expectedProofSpecs, tmClient.ProofSpecs) {
369-
return errorsmod.Wrapf(types.ErrInvalidClient, "client has invalid proof specs. expected: %v got: %v",
370-
expectedProofSpecs, tmClient.ProofSpecs)
371-
}
372-
373-
if err := light.ValidateTrustLevel(tmClient.TrustLevel.ToTendermint()); err != nil {
374-
return errorsmod.Wrapf(types.ErrInvalidClient, "trust-level invalid: %v", err)
375-
}
376-
377-
expectedUbdPeriod, err := k.stakingKeeper.UnbondingTime(ctx)
378-
if err != nil {
379-
return errorsmod.Wrapf(err, "failed to retrieve unbonding period")
380-
}
381-
382-
if expectedUbdPeriod != tmClient.UnbondingPeriod {
383-
return errorsmod.Wrapf(types.ErrInvalidClient, "invalid unbonding period. expected: %s, got: %s",
384-
expectedUbdPeriod, tmClient.UnbondingPeriod)
385-
}
386-
387-
if tmClient.UnbondingPeriod < tmClient.TrustingPeriod {
388-
return errorsmod.Wrapf(types.ErrInvalidClient, "unbonding period must be greater than trusting period. unbonding period (%d) < trusting period (%d)",
389-
tmClient.UnbondingPeriod, tmClient.TrustingPeriod)
390-
}
391-
392-
if len(tmClient.UpgradePath) != 0 {
393-
// For now, SDK IBC implementation assumes that upgrade path (if defined) is defined by SDK upgrade module
394-
expectedUpgradePath := []string{upgradetypes.StoreKey, upgradetypes.KeyUpgradedIBCState}
395-
if !reflect.DeepEqual(expectedUpgradePath, tmClient.UpgradePath) {
396-
return errorsmod.Wrapf(types.ErrInvalidClient, "upgrade path must be the upgrade path defined by upgrade module. expected %v, got %v",
397-
expectedUpgradePath, tmClient.UpgradePath)
398-
}
399-
}
400-
return nil
323+
return k.consensusHost.ValidateSelfClient(ctx, clientState)
401324
}
402325

403326
// GetUpgradePlan executes the upgrade keeper GetUpgradePlan function.

modules/core/02-client/keeper/keeper_test.go

+2-116
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"
2626
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
2727
"github.com/cosmos/ibc-go/v8/modules/core/exported"
28-
solomachine "github.com/cosmos/ibc-go/v8/modules/light-clients/06-solomachine"
2928
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
3029
localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost"
3130
ibctesting "github.com/cosmos/ibc-go/v8/testing"
@@ -45,10 +44,7 @@ const (
4544
maxClockDrift time.Duration = time.Second * 10
4645
)
4746

48-
var (
49-
testClientHeight = types.NewHeight(0, 5)
50-
testClientHeightRevision1 = types.NewHeight(1, 5)
51-
)
47+
var testClientHeight = types.NewHeight(0, 5)
5248

5349
type KeeperTestSuite struct {
5450
testifysuite.Suite
@@ -85,7 +81,7 @@ func (suite *KeeperTestSuite) SetupTest() {
8581

8682
suite.cdc = app.AppCodec()
8783
suite.ctx = app.BaseApp.NewContext(isCheckTx)
88-
suite.keeper = &app.IBCKeeper.ClientKeeper
84+
suite.keeper = app.IBCKeeper.ClientKeeper
8985
suite.privVal = cmttypes.NewMockPV()
9086
pubKey, err := suite.privVal.GetPubKey()
9187
suite.Require().NoError(err)
@@ -145,90 +141,6 @@ func (suite *KeeperTestSuite) TestSetClientConsensusState() {
145141
suite.Require().Equal(suite.consensusState, tmConsState, "ConsensusState not stored correctly")
146142
}
147143

148-
func (suite *KeeperTestSuite) TestValidateSelfClient() {
149-
testClientHeight := types.GetSelfHeight(suite.chainA.GetContext())
150-
testClientHeight.RevisionHeight--
151-
152-
testCases := []struct {
153-
name string
154-
clientState exported.ClientState
155-
expPass bool
156-
}{
157-
{
158-
"success",
159-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
160-
true,
161-
},
162-
{
163-
"success with nil UpgradePath",
164-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), nil),
165-
true,
166-
},
167-
{
168-
"frozen client",
169-
&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},
170-
false,
171-
},
172-
{
173-
"incorrect chainID",
174-
ibctm.NewClientState("gaiatestnet", ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
175-
false,
176-
},
177-
{
178-
"invalid client height",
179-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, types.GetSelfHeight(suite.chainA.GetContext()).Increment().(types.Height), commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
180-
false,
181-
},
182-
{
183-
"invalid client type",
184-
solomachine.NewClientState(0, &solomachine.ConsensusState{PublicKey: suite.solomachine.ConsensusState().PublicKey, Diversifier: suite.solomachine.Diversifier, Timestamp: suite.solomachine.Time}),
185-
false,
186-
},
187-
{
188-
"invalid client revision",
189-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeightRevision1, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
190-
false,
191-
},
192-
{
193-
"invalid proof specs",
194-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, nil, ibctesting.UpgradePath),
195-
false,
196-
},
197-
{
198-
"invalid trust level",
199-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.Fraction{Numerator: 0, Denominator: 1}, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath), false,
200-
},
201-
{
202-
"invalid unbonding period",
203-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod+10, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
204-
false,
205-
},
206-
{
207-
"invalid trusting period",
208-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, ubdPeriod+10, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath),
209-
false,
210-
},
211-
{
212-
"invalid upgrade path",
213-
ibctm.NewClientState(suite.chainA.ChainID, ibctm.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), []string{"bad", "upgrade", "path"}),
214-
false,
215-
},
216-
}
217-
218-
for _, tc := range testCases {
219-
tc := tc
220-
221-
suite.Run(tc.name, func() {
222-
err := suite.chainA.App.GetIBCKeeper().ClientKeeper.ValidateSelfClient(suite.chainA.GetContext(), tc.clientState)
223-
if tc.expPass {
224-
suite.Require().NoError(err, "expected valid client for case: %s", tc.name)
225-
} else {
226-
suite.Require().Error(err, "expected invalid client for case: %s", tc.name)
227-
}
228-
})
229-
}
230-
}
231-
232144
func (suite *KeeperTestSuite) TestGetAllGenesisClients() {
233145
clientIDs := []string{
234146
exported.LocalhostClientID, testClientID2, testClientID3, testClientID,
@@ -308,32 +220,6 @@ func (suite *KeeperTestSuite) TestGetAllGenesisMetadata() {
308220
})
309221
}
310222

311-
func (suite *KeeperTestSuite) TestGetConsensusState() {
312-
suite.ctx = suite.ctx.WithBlockHeight(10)
313-
cases := []struct {
314-
name string
315-
height types.Height
316-
expPass bool
317-
}{
318-
{"zero height", types.ZeroHeight(), false},
319-
{"height > latest height", types.NewHeight(0, uint64(suite.ctx.BlockHeight())+1), false},
320-
{"latest height - 1", types.NewHeight(0, uint64(suite.ctx.BlockHeight())-1), true},
321-
{"latest height", types.GetSelfHeight(suite.ctx), true},
322-
}
323-
324-
for i, tc := range cases {
325-
tc := tc
326-
cs, err := suite.keeper.GetSelfConsensusState(suite.ctx, tc.height)
327-
if tc.expPass {
328-
suite.Require().NoError(err, "Case %d should have passed: %s", i, tc.name)
329-
suite.Require().NotNil(cs, "Case %d should have passed: %s", i, tc.name)
330-
} else {
331-
suite.Require().Error(err, "Case %d should have failed: %s", i, tc.name)
332-
suite.Require().Nil(cs, "Case %d should have failed: %s", i, tc.name)
333-
}
334-
}
335-
}
336-
337223
// 2 clients in total are created on chainA. The first client is updated so it contains an initial consensus state
338224
// and a consensus state at the update height.
339225
func (suite *KeeperTestSuite) TestGetAllConsensusStates() {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99

1010
// Migrator is a struct for handling in-place store migrations.
1111
type Migrator struct {
12-
keeper Keeper
12+
keeper *Keeper
1313
}
1414

1515
// NewMigrator returns a new Migrator.
16-
func NewMigrator(keeper Keeper) Migrator {
16+
func NewMigrator(keeper *Keeper) Migrator {
1717
return Migrator{keeper: keeper}
1818
}
1919

modules/core/02-client/migrations/v7/genesis_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (suite *MigrationsV7TestSuite) TestMigrateGenesisSolomachine() {
3333
solomachine := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, ibctesting.DefaultSolomachineClientID, "testing", 1)
3434
solomachineMulti := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, "06-solomachine-1", "testing", 4)
3535

36-
clientGenState := ibcclient.ExportGenesis(suite.chainA.GetContext(), suite.chainA.App.GetIBCKeeper().ClientKeeper)
36+
clientGenState := ibcclient.ExportGenesis(suite.chainA.GetContext(), *suite.chainA.App.GetIBCKeeper().ClientKeeper)
3737

3838
// manually generate old proto buf definitions and set in genesis
3939
// NOTE: we cannot use 'ExportGenesis' for the solo machines since we are
@@ -108,7 +108,7 @@ func (suite *MigrationsV7TestSuite) TestMigrateGenesisSolomachine() {
108108
// NOTE: tendermint clients are not pruned in genesis so the test should not have expired tendermint clients
109109
err := v7.MigrateStore(suite.chainA.GetContext(), suite.chainA.GetSimApp().GetKey(ibcexported.StoreKey), suite.chainA.App.AppCodec(), suite.chainA.GetSimApp().IBCKeeper.ClientKeeper)
110110
suite.Require().NoError(err)
111-
expectedClientGenState := ibcclient.ExportGenesis(suite.chainA.GetContext(), suite.chainA.App.GetIBCKeeper().ClientKeeper)
111+
expectedClientGenState := ibcclient.ExportGenesis(suite.chainA.GetContext(), *suite.chainA.App.GetIBCKeeper().ClientKeeper)
112112

113113
cdc, ok := suite.chainA.App.AppCodec().(codec.ProtoCodecMarshaler)
114114
suite.Require().True(ok)

modules/core/02-client/proposal_handler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
//
1616
// Deprecated: This function is deprecated and will be removed in a future release.
1717
// Please use MsgRecoverClient and MsgIBCSoftwareUpgrade in favour of this legacy Handler.
18-
func NewClientProposalHandler(k keeper.Keeper) govtypes.Handler { //nolint:staticcheck
18+
func NewClientProposalHandler(k *keeper.Keeper) govtypes.Handler { //nolint:staticcheck
1919
return func(ctx sdk.Context, content govtypes.Content) error {
2020
switch c := content.(type) {
2121
case *types.ClientUpdateProposal:

0 commit comments

Comments
 (0)