Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add verifiable upgrade type and validate basic functions #3451

12 changes: 12 additions & 0 deletions modules/core/03-connection/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ func (k Keeper) HasConnection(ctx sdk.Context, connectionID string) bool {
return store.Has(host.ConnectionKey(connectionID))
}

// CheckIsOpen returns nil if the connection with the given id is OPEN. Otherwise an error is returned.
func (k Keeper) CheckIsOpen(ctx sdk.Context, connectionID string) error {
damiannolan marked this conversation as resolved.
Show resolved Hide resolved
connection, found := k.GetConnection(ctx, connectionID)
if !found {
return errorsmod.Wrapf(types.ErrConnectionNotFound, "failed to retrieve connection: %s", connectionID)
}
if connection.State != types.OPEN {
return errorsmod.Wrapf(types.ErrInvalidConnectionState, "connectionID (%s) state is not OPEN (got %s)", connectionID, connection.State.String())
}
return nil
}

// SetConnection sets a connection to the store
func (k Keeper) SetConnection(ctx sdk.Context, connectionID string, connection types.ConnectionEnd) {
store := ctx.KVStore(k.storeKey)
Expand Down
62 changes: 62 additions & 0 deletions modules/core/03-connection/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/cosmos/ibc-go/v7/modules/core/03-connection/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"
ibctesting "github.com/cosmos/ibc-go/v7/testing"
)
Expand Down Expand Up @@ -176,3 +177,64 @@ func (suite *KeeperTestSuite) TestLocalhostConnectionEndCreation() {
expectedCounterParty := types.NewCounterparty(exported.LocalhostClientID, exported.LocalhostConnectionID, commitmenttypes.NewMerklePrefix(connectionKeeper.GetCommitmentPrefix().Bytes()))
suite.Require().Equal(expectedCounterParty, connectionEnd.Counterparty)
}

func (suite *KeeperTestSuite) TestCheckIsOpen() {
var path *ibctesting.Path

cases := []struct {
msg string
malleate func()
expPass bool
}{
{
msg: "success",
malleate: func() {},
expPass: true,
},
{
msg: "uninitialized connection",
malleate: func() {
connection := path.EndpointA.GetConnection()
connection.State = types.UNINITIALIZED
path.EndpointA.SetConnection(connection)
},
expPass: false,
},
{
msg: "init connection",
malleate: func() {
connection := path.EndpointA.GetConnection()
connection.State = types.INIT
path.EndpointA.SetConnection(connection)
},
expPass: false,
},
{
msg: "no connection",
malleate: func() {
storeKey := suite.chainA.GetSimApp().GetKey(exported.StoreKey)
kvStore := suite.chainA.GetContext().KVStore(storeKey)
kvStore.Delete(host.ConnectionKey(ibctesting.FirstConnectionID))
},
expPass: false,
},
}

for _, tc := range cases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path = ibctesting.NewPath(suite.chainA, suite.chainB)
suite.coordinator.Setup(path)

tc.malleate()

connectionKeeper := suite.chainA.GetSimApp().IBCKeeper.ConnectionKeeper

if tc.expPass {
suite.Require().NoError(connectionKeeper.CheckIsOpen(suite.chainA.GetContext(), ibctesting.FirstConnectionID))
} else {
suite.Require().Error(connectionKeeper.CheckIsOpen(suite.chainA.GetContext(), ibctesting.FirstConnectionID))
}
})
}
}
27 changes: 26 additions & 1 deletion modules/core/04-channel/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package keeper

import (
"reflect"
"strconv"
"strings"

errorsmod "cosmossdk.io/errors"
db "github.com/cometbft/cometbft-db"
"github.com/cometbft/cometbft/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
errorsmod "github.com/cosmos/cosmos-sdk/types/errors"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"

clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
Expand Down Expand Up @@ -571,6 +572,30 @@ func (k Keeper) DeleteUpgradeTimeout(ctx sdk.Context, portID, channelID string)
store.Delete(host.ChannelUpgradeTimeoutKey(portID, channelID))
}

// ValidateProposedUpgradeFields validates the proposed upgrade fields against the existing channel.
// It returns an error if the following constraints are not met:
// - there exists at least one valid proposed change to the existing channel fields
// - the proposed order is a subset of the existing order
// - the proposed connection hops do not exist
// - the proposed version is non-empty (checked in ModifiableUpgradeFields.ValidateBasic())
damiannolan marked this conversation as resolved.
Show resolved Hide resolved
// - the proposed connection hops are not open
func (k Keeper) ValidateProposedUpgradeFields(ctx sdk.Context, proposedUpgrade types.ModifiableUpgradeFields, existingChannel types.Channel) error {
damiannolan marked this conversation as resolved.
Show resolved Hide resolved
currentFields := types.ModifiableUpgradeFields{
Ordering: existingChannel.Ordering,
ConnectionHops: existingChannel.ConnectionHops,
Version: existingChannel.Version,
}
if reflect.DeepEqual(proposedUpgrade, currentFields) {
return errorsmod.Wrap(types.ErrChannelExists, "existing channel end is identical to proposed upgrade channel end")
}

if !currentFields.Ordering.SubsetOf(proposedUpgrade.Ordering) {
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
return errorsmod.Wrap(types.ErrInvalidChannelOrdering, "channel ordering must be a subset of the new ordering")
}

return k.connectionKeeper.CheckIsOpen(ctx, proposedUpgrade.ConnectionHops[0])
Copy link
Contributor

@damiannolan damiannolan Apr 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I prefer just doing the check explicitly in here rather than adding another expected keeper interface method but happy to keep this as is if there's consensus on adding this instead.
My vote would be for doing the check directly here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that this could be used in many other places, it also covers the not found case. Quickly looking it looks like we have around 7 other duplicate checks.

My preference would be to re-use this helper

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @damiannolan. I think also the CheckIsOpen function should anyway return a boolean (unless we rename it to CheckExistsAndIsOpen, since it is actually checking two things). In my opinion I would just do the explicit check here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there was a bit of bike-shedding over the name/scope of this function between us. Initially we had it as IsOpen with a single boolean return but that didn't make it as self contained as we'd like. Returning the error allowed it to basically also check for any required error conditions and return them, simplifying the validation, hence the rename to CheckIsOpen.

Cian did say this function might not fly with everyone though 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to just remove the function entirely and do the explicit checking where needed! 👍

}

// common functionality for IteratePacketCommitment and IteratePacketAcknowledgement
func (k Keeper) iterateHashes(ctx sdk.Context, iterator db.Iterator, cb func(portID, channelID string, sequence uint64, hash []byte) bool) {
defer sdk.LogDeferred(ctx.Logger(), func() error { return iterator.Close() })
Expand Down
88 changes: 88 additions & 0 deletions modules/core/04-channel/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (

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"
"github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/v7/modules/core/24-host"
"github.com/cosmos/ibc-go/v7/modules/core/exported"
ibctesting "github.com/cosmos/ibc-go/v7/testing"
ibcmock "github.com/cosmos/ibc-go/v7/testing/mock"
)
Expand Down Expand Up @@ -557,3 +560,88 @@ func (suite *KeeperTestSuite) TestUpgradeTimeoutAccessors() {
suite.Require().False(found)
})
}

func (suite *KeeperTestSuite) TestValidateProposedUpgradeFields() {
var (
proposedUpgrade *types.ModifiableUpgradeFields
path *ibctesting.Path
)

tests := []struct {
name string
malleate func()
expPass bool
}{
{
name: "modify ordering to invalid value",
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved
malleate: func() {
proposedUpgrade.Ordering = types.ORDERED
},
expPass: false,
},
{
name: "no modified fields in proposed upgrade",
malleate: func() {},
expPass: false,
},
{
name: "change channel version",
malleate: func() {
proposedUpgrade.Version = "1.0.0"
},
expPass: true,
},
{
name: "change connection hops",
malleate: func() {
path := ibctesting.NewPath(suite.chainA, suite.chainB)
suite.coordinator.Setup(path)
proposedUpgrade.ConnectionHops = []string{path.EndpointA.ConnectionID}
},
expPass: true,
},
{
name: "attempt upgrade when connection is not set",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name: "attempt upgrade when connection is not set",
name: "fails when connection is not set",

malleate: func() {
storeKey := suite.chainA.GetSimApp().GetKey(exported.StoreKey)
kvStore := suite.chainA.GetContext().KVStore(storeKey)
kvStore.Delete(host.ConnectionKey(ibctesting.FirstConnectionID))
},
expPass: false,
},
{
name: "attempt upgrade when connection is not open",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name: "attempt upgrade when connection is not open",
name: "fails when connection is not open",

malleate: func() {
connection := path.EndpointA.GetConnection()
connection.State = connectiontypes.UNINITIALIZED
path.EndpointA.SetConnection(connection)
},
expPass: false,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should add test cases for empty channel version, packet sent and the upgrade timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be tested in a separate test that tests validate basic. This PR is already quite large, are you happy if we create a follow up PR to add tests for ValidateBasic of UpgradeFields and Upgrade?

}

for _, tc := range tests {
tc := tc
suite.Run(tc.name, func() {
suite.SetupTest()
path = ibctesting.NewPath(suite.chainA, suite.chainB)
suite.coordinator.Setup(path)

existingChannel := path.EndpointA.GetChannel()
proposedUpgrade = &types.ModifiableUpgradeFields{
Ordering: existingChannel.Ordering,
ConnectionHops: existingChannel.ConnectionHops,
Version: existingChannel.Version,
}

tc.malleate()

err := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.ValidateProposedUpgradeFields(suite.chainA.GetContext(), *proposedUpgrade, existingChannel)
if tc.expPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
}
})
}
}
1 change: 1 addition & 0 deletions modules/core/04-channel/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ var (
ErrInvalidUpgradeTimeout = errorsmod.Register(SubModuleName, 27, "either timeout height or timeout timestamp must be non-zero")
ErrUpgradeSequenceNotFound = errorsmod.Register(SubModuleName, 28, "upgrade sequence not found")
ErrUpgradeErrorNotFound = errorsmod.Register(SubModuleName, 29, "upgrade error receipt not found")
ErrInvalidUpgrade = errorsmod.Register(SubModuleName, 30, "invalid upgrade")
)
2 changes: 2 additions & 0 deletions modules/core/04-channel/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type ClientKeeper interface {

// ConnectionKeeper expected account IBC connection keeper
type ConnectionKeeper interface {
CheckIsOpen(ctx sdk.Context, connectionID string) error
HasConnection(ctx sdk.Context, connectionID string) bool
damiannolan marked this conversation as resolved.
Show resolved Hide resolved
GetConnection(ctx sdk.Context, connectionID string) (connectiontypes.ConnectionEnd, bool)
GetTimestampAtHeight(
ctx sdk.Context,
Expand Down
43 changes: 43 additions & 0 deletions modules/core/04-channel/types/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package types

import (
errorsmod "cosmossdk.io/errors"

"github.com/cosmos/ibc-go/v7/internal/collections"
)

// ValidateBasic performs a basic validation of the upgrade fields
func (u Upgrade) ValidateBasic() error {
if err := u.UpgradeFields.ValidateBasic(); err != nil {
return errorsmod.Wrap(err, "proposed upgrade fields are invalid")
}

if !u.Timeout.IsValid() {
return errorsmod.Wrap(ErrInvalidUpgrade, "upgrade timeout cannot be empty")
}

// TODO: determine if last packet sequence sent can be 0?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leave this todo in? I remember we talked about it being sensible if its zero (i.e channel which has just opened) but maybe it makes sense to leave it around for another pair of eyes to check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think 0 should technically be a valid value (no packets sent yet), maybe @colin-axner / @AdityaSripal could confirm if we should let 0 be an accepted value here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packet sequences start at 1. But iirc from yesterday this value can be determined within the state machine in WriteChanUpgradeInit() as opposed to expecting a user provided value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate basic of the message should then check for >= 1 right? Even if it is determined within the state machine it should still be validated here if possible I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove this function? I think we should have MsgUpgradeInit and MsgUpgradeTry use the UpgradeFields and UpgradeTimeout directly rather than wrapping with Upgrade. I suggest this because it seems unnecessary to expose a field populated by the state machine to the relayer. This would render this ValidateBasic unnecessary as the msg should do basic validation on the fields and timeout and then the state machine would construct the upgrade type with the fields, timeout and retrieved packet sequence send. I guess we could keep if for the situation where the entire counterparty upgrade type is provided in the TRY and ACK, in this situation the sequence should not equal 0.

My vote is to modify the init/try field for self upgrade to use the fields/timeout directly in the msg type and add a if sequence != 0 check to be used to verify the fully formed counterparty upgrade

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm happy enough with that approach 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, just realising the message modification will cause a bigger diff with the OnChanInit keeper func which is being addressed in a follow up. @colin-axner are you happy if the message structure gets updated (and this validate basic removed) in a follow up?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely!

return nil
}

// ValidateBasic performs a basic validation of the proposed upgrade fields
func (muf ModifiableUpgradeFields) ValidateBasic() error {
if !collections.Contains(muf.Ordering, []Order{ORDERED, UNORDERED}) {
return errorsmod.Wrap(ErrInvalidChannelOrdering, muf.Ordering.String())
}

if len(muf.ConnectionHops) != 1 {
return errorsmod.Wrap(ErrTooManyConnectionHops, "current IBC version only supports one connection hop")
}

if muf.Version == "" {
return errorsmod.Wrap(ErrInvalidUpgrade, "proposed upgrade version cannot be empty")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return errorsmod.Wrap(ErrInvalidUpgrade, "proposed upgrade version cannot be empty")
return errorsmod.Wrap(ErrInvalidVersion, "version cannot be empty")

}
damiannolan marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

// IsValid returns true if either the height or timestamp is non-zero
func (ut UpgradeTimeout) IsValid() bool {
return !ut.TimeoutHeight.IsZero() || ut.TimeoutTimestamp != 0
}
Loading