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

Refactor ChanUpgradeInit to use new upgrade type #3456

Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
995c1f1
added validateProposedUpgradeFields function and CheckIsOpen helper f…
colin-axner Apr 12, 2023
f8f6ad4
added unit test for CheckIsOpen
chatton Apr 13, 2023
808ec70
adding unit tests for ValidateProposedUpgradeFields
chatton Apr 13, 2023
8dd0c44
fix linter
chatton Apr 13, 2023
64878ae
update to use errorsmod instead of sdk errors
chatton Apr 13, 2023
51bb7a7
added upgrade verification function and unit tests
chatton Apr 13, 2023
f203df9
improved variable naming
chatton Apr 13, 2023
837e3c9
Merge branch '04-channel-upgrades' into cian/issue#3445-add-verifiabl…
chatton Apr 13, 2023
5f9ad67
re-arranged order of test functions
chatton Apr 13, 2023
7bdf33c
Merge branch 'cian/issue#3445-add-verifiable-upgrade-type-and-validat…
chatton Apr 13, 2023
13812eb
refactor chan open init
chatton Apr 13, 2023
2f7d0fc
removed commented out code
chatton Apr 13, 2023
28779f5
rename ValidateProposedUpgradeFields to ValidateUpgradeFields
chatton Apr 18, 2023
08af922
merge 04-channel-upgrades
chatton Apr 18, 2023
458b171
merge 04-channel-upgrades branch
chatton Apr 18, 2023
93db50e
refactoring to use new upgrade type
chatton Apr 18, 2023
b83849a
removed unused functions and tests
chatton Apr 18, 2023
f29e2f8
remove unnecessary upgrade prefix
chatton Apr 18, 2023
09ddf84
addressing PR feedback
chatton Apr 18, 2023
8f36c16
fix linter
chatton Apr 18, 2023
0b36176
fixed logging messages
crodriguezvega Apr 19, 2023
dfd9be3
wip: pr comments
chatton Apr 19, 2023
f7eb3dd
Merge branch 'cian/issue#3447-modify-upgradeinit-to-use-new-upgrade-t…
chatton Apr 19, 2023
c631e06
pr feedback: use connection id instead of channel id
chatton Apr 19, 2023
b8b5ca4
pr feedback: refactor MsgChannelUpgradeInitResponse to contain Upgrad…
chatton Apr 19, 2023
0e8092b
pr feedback: adjusting error messages and test case names
chatton Apr 19, 2023
06b1271
explicitly check for INITUPGRADE state
chatton Apr 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions e2e/tests/core/04-channel/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"testing"

test "github.com/strangelove-ventures/interchaintest/v7/testutil"
"github.com/stretchr/testify/suite"

"github.com/cosmos/ibc-go/e2e/testsuite"
Expand All @@ -30,18 +31,23 @@ func (s *ChannelUpgradeTestSuite) TestChannelUpgrade() {

rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount)

counterParty := channeltypes.NewCounterparty(channelA.Counterparty.PortID, channelA.Counterparty.ChannelID)
proposedUpgradeChannel := channeltypes.NewChannel(channeltypes.INITUPGRADE, channeltypes.UNORDERED, counterParty, channelA.ConnectionHops, `{"fee_version":"ics29-1","app_version":"ics20-1"}`)

t.Run("channel upgrade init", func(t *testing.T) {
upgradeTimeout := channeltypes.NewUpgradeTimeout(clienttypes.NewHeight(0, 10000), 0)
upgradeFields := channeltypes.NewUpgradeFields(channeltypes.UNORDERED, channelA.ConnectionHops, `{"fee_version":"ics29-1","app_version":"ics20-1"}`)
msgChanUpgradeInit := channeltypes.NewMsgChannelUpgradeInit(
channelA.PortID, channelA.ChannelID, proposedUpgradeChannel, clienttypes.NewHeight(0, 10000), 0, rlyWallet.FormattedAddress(),
channelA.PortID, channelA.ChannelID, upgradeFields, upgradeTimeout, rlyWallet.FormattedAddress(),
)

s.Require().NoError(msgChanUpgradeInit.ValidateBasic())

txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanUpgradeInit)
s.Require().NoError(err)
s.AssertValidTxResponse(txResp)

s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA))
colin-axner marked this conversation as resolved.
Show resolved Hide resolved

channel, err := s.QueryChannel(ctx, chainA, channelA.PortID, channelA.ChannelID)
s.Require().NoError(err)
s.Require().Equal(channeltypes.INITUPGRADE, channel.State)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (im IBCMiddleware) OnTimeoutPacket(
}

// OnChanUpgradeInit implements the IBCModule interface
func (im IBCMiddleware) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, counterparty channeltypes.Counterparty, version, previousVersion string) (string, error) {
func (im IBCMiddleware) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, version, previousVersion string) (string, error) {
return icatypes.Version, nil
}

Expand Down
2 changes: 1 addition & 1 deletion modules/apps/27-interchain-accounts/host/ibc_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (im IBCModule) OnTimeoutPacket(
}

// OnChanUpgradeInit implements the IBCModule interface
func (im IBCModule) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, counterparty channeltypes.Counterparty, version, previousVersion string) (string, error) {
func (im IBCModule) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, version, previousVersion string) (string, error) {
return "", errorsmod.Wrap(icatypes.ErrInvalidChannelFlow, "channel handshake must be initiated by controller chain")
}

Expand Down
4 changes: 2 additions & 2 deletions modules/apps/29-fee/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ func (im IBCMiddleware) OnTimeoutPacket(
}

// OnChanUpgradeInit implements the IBCModule interface
func (im IBCMiddleware) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, counterparty channeltypes.Counterparty, version, previousVersion string) (string, error) {
return im.app.OnChanUpgradeInit(ctx, order, connectionHops, portID, channelID, sequence, counterparty, version, previousVersion)
func (im IBCMiddleware) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, version, previousVersion string) (string, error) {
return im.app.OnChanUpgradeInit(ctx, order, connectionHops, portID, channelID, sequence, version, previousVersion)
}

// OnChanUpgradeTry implement s the IBCModule interface
Expand Down
2 changes: 1 addition & 1 deletion modules/apps/transfer/ibc_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func (im IBCModule) OnTimeoutPacket(
}

// OnChanUpgradeInit implements the IBCModule interface
func (im IBCModule) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, counterparty channeltypes.Counterparty, version, previousVersion string) (string, error) {
func (im IBCModule) OnChanUpgradeInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, sequence uint64, version, previousVersion string) (string, error) {
return types.Version, nil
}

Expand Down
13 changes: 7 additions & 6 deletions modules/core/04-channel/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,17 +273,18 @@ func emitChannelClosedEvent(ctx sdk.Context, packet exported.PacketI, channel ty
}

// emitChannelUpgradeInitEvent emits a channel upgrade init event
func emitChannelUpgradeInitEvent(ctx sdk.Context, portID string, channelID string, upgradeSequence uint64, channel types.Channel) {
func emitChannelUpgradeInitEvent(ctx sdk.Context, portID string, channelID string, currentChannel types.Channel, upgrade types.Upgrade) {
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeChannelUpgradeInit,
sdk.NewAttribute(types.AttributeKeyPortID, portID),
sdk.NewAttribute(types.AttributeKeyChannelID, channelID),
sdk.NewAttribute(types.AttributeCounterpartyPortID, channel.Counterparty.PortId),
sdk.NewAttribute(types.AttributeCounterpartyChannelID, channel.Counterparty.ChannelId),
sdk.NewAttribute(types.AttributeKeyConnectionID, channel.ConnectionHops[0]),
sdk.NewAttribute(types.AttributeVersion, channel.Version),
sdk.NewAttribute(types.AttributeKeyUpgradeSequence, fmt.Sprintf("%d", upgradeSequence)),
sdk.NewAttribute(types.AttributeCounterpartyPortID, currentChannel.Counterparty.PortId),
sdk.NewAttribute(types.AttributeCounterpartyChannelID, currentChannel.Counterparty.ChannelId),
sdk.NewAttribute(types.AttributeKeyUpgradeConnectionHops, upgrade.Fields.ConnectionHops[0]),
sdk.NewAttribute(types.AttributeKeyUpgradeVersion, upgrade.Fields.Version),
sdk.NewAttribute(types.AttributeKeyUpgradeSequence, fmt.Sprintf("%d", currentChannel.UpgradeSequence)),
sdk.NewAttribute(types.AttributeKeyUpgradeOrdering, upgrade.Fields.Ordering.String()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to add a test for this event, so that if in the future new upgrade fields are added, then the test should fail, so that we don't forget to add the field here. Alternatively, instead of adding each upgrade field with its own key, would it work to have one key for all the upgrade fields and the value is a JSON-encoded string of all of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you happy if we do this in a followup, I think a few changes would need to happen to the tests (as we are testing the channel keeper function but events are emitted at the message server layer)

),
sdk.NewEvent(
sdk.EventTypeMessage,
Expand Down
2 changes: 1 addition & 1 deletion modules/core/04-channel/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ func (suite *KeeperTestSuite) TestUpgradeTimeoutAccessors() {
suite.coordinator.SetupConnections(path)
suite.coordinator.CreateChannels(path)

expUpgradeTimeout := types.UpgradeTimeout{TimeoutHeight: clienttypes.NewHeight(1, 10), TimeoutTimestamp: uint64(suite.coordinator.CurrentTime.UnixNano())}
expUpgradeTimeout := types.UpgradeTimeout{Height: clienttypes.NewHeight(1, 10), Timestamp: uint64(suite.coordinator.CurrentTime.UnixNano())}

suite.Run("set upgrade timeout", func() {
upgradeTimeout, found := suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetUpgradeTimeout(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
Expand Down
129 changes: 35 additions & 94 deletions modules/core/04-channel/keeper/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
package keeper

import (
"reflect"

errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/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"
portkeeper "github.com/cosmos/ibc-go/v7/modules/core/05-port/keeper"
porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types"
host "github.com/cosmos/ibc-go/v7/modules/core/24-host"
)

// ChanUpgradeInit is called by a module to initiate a channel upgrade handshake with
Expand All @@ -22,115 +14,64 @@ func (k Keeper) ChanUpgradeInit(
ctx sdk.Context,
portID string,
channelID string,
chanCap *capabilitytypes.Capability,
proposedUpgradeChannel types.Channel,
counterpartyTimeoutHeight clienttypes.Height,
counterpartyTimeoutTimestamp uint64,
) (upgradeSequence uint64, previousVersion string, err error) {
upgradeFields types.UpgradeFields,
upgradeTimeout types.UpgradeTimeout,
) (proposedUpgrade types.Upgrade, err error) {
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
channel, found := k.GetChannel(ctx, portID, channelID)
if !found {
return 0, "", errorsmod.Wrapf(types.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
return proposedUpgrade, errorsmod.Wrapf(types.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}

if channel.State != types.OPEN {
return 0, "", errorsmod.Wrapf(types.ErrInvalidChannelState, "expected %s, got %s", types.OPEN, channel.State)
}

if !k.scopedKeeper.AuthenticateCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)) {
return 0, "", errorsmod.Wrapf(types.ErrChannelCapabilityNotFound, "caller does not own capability for channel, port ID (%s) channel ID (%s)", portID, channelID)
return proposedUpgrade, errorsmod.Wrapf(types.ErrInvalidChannelState, "expected %s, got %s", types.OPEN, channel.State)
}

// set the restore channel to the current channel and reassign channel state to INITUPGRADE,
// if the channel == proposedUpgradeChannel then fail fast as no upgradable fields have been modified.
restoreChannel := channel
channel.State = types.INITUPGRADE
if reflect.DeepEqual(channel, proposedUpgradeChannel) {
return 0, "", errorsmod.Wrap(types.ErrChannelExists, "existing channel end is identical to proposed upgrade channel end")
if err := k.ValidateUpgradeFields(ctx, upgradeFields, channel); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

In a separate pr, how would folks feel about making this function private and placed in upgrade.go?

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 I made the exported as it was used in another package, if it's possible though I'm in favour of making it unexported.

Copy link
Contributor

Choose a reason for hiding this comment

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

What package? I feel like validation of the upgrade fields against the current fields should only happen in the init and try handlers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it looks like here

return proposedUpgrade, err
}

connectionEnd, err := k.GetConnection(ctx, proposedUpgradeChannel.ConnectionHops[0])
proposedUpgrade, err = k.constructProposedUpgrade(ctx, portID, channelID, upgradeFields, upgradeTimeout)
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, "", err
}

if connectionEnd.GetState() != int32(connectiontypes.OPEN) {
return 0, "", errorsmod.Wrapf(
connectiontypes.ErrInvalidConnectionState,
"connection state is not OPEN (got %s)", connectiontypes.State(connectionEnd.GetState()).String(),
)
}

if proposedUpgradeChannel.Counterparty.PortId != channel.Counterparty.PortId ||
proposedUpgradeChannel.Counterparty.ChannelId != channel.Counterparty.ChannelId {
return 0, "", errorsmod.Wrap(types.ErrInvalidCounterparty, "counterparty port ID and channel ID cannot be upgraded")
return proposedUpgrade, errorsmod.Wrap(err, "failed to construct proposed upgrade")
}

if !proposedUpgradeChannel.Ordering.SubsetOf(channel.Ordering) {
return 0, "", errorsmod.Wrap(types.ErrInvalidChannelOrdering, "channel ordering must be a subset of the new ordering")
}

upgradeSequence = uint64(1)
if seq, found := k.GetUpgradeSequence(ctx, portID, channelID); found {
upgradeSequence = seq + 1
}

upgradeTimeout := types.UpgradeTimeout{
TimeoutHeight: counterpartyTimeoutHeight,
TimeoutTimestamp: counterpartyTimeoutTimestamp,
}

k.SetUpgradeRestoreChannel(ctx, portID, channelID, restoreChannel)
k.SetUpgradeSequence(ctx, portID, channelID, upgradeSequence)
k.SetUpgradeTimeout(ctx, portID, channelID, upgradeTimeout)
channel.UpgradeSequence++
charleenfei marked this conversation as resolved.
Show resolved Hide resolved
k.SetChannel(ctx, portID, channelID, channel)

return upgradeSequence, channel.Version, nil
return proposedUpgrade, nil
}

// WriteUpgradeInitChannel writes a channel which has successfully passed the UpgradeInit handshake step.
// An event is emitted for the handshake step.
func (k Keeper) WriteUpgradeInitChannel(
ctx sdk.Context,
portID,
channelID string,
upgradeSequence uint64,
channelUpgrade types.Channel,
) {
func (k Keeper) WriteUpgradeInitChannel(ctx sdk.Context, portID, channelID string, currentChannel types.Channel, upgrade types.Upgrade) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The only thing we have to be careful with about passing in the channel is to ensure it is obtained and not modified in state before calling this function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we could do an additional read of the channel directly in the function, I'm not sure it's necessary though, WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

It is probably safer to do an additional read but I'm also happy to leave as is for now. @colin-axner and I discussed yesterday briefly about refactoring grpc handlers into their associated submodules rather than having the entry point at the core layer. This would mean we could reduce the exported APIs and prevent them from being misused. If this were a private method then it would be much safer passing currentChannel.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of doing the additional read for safety and then optimizing later if there's a clean way to do so, but I don't have a strong preference here. Will go with your gut!

defer telemetry.IncrCounter(1, "ibc", "channel", "upgrade-init")

k.SetChannel(ctx, portID, channelID, channelUpgrade)
k.Logger(ctx).Info("channel state updated", "port-id", portID, "channel-id", channelID, "previous-state", types.OPEN.String(), "new-state", types.INITUPGRADE.String())

emitChannelUpgradeInitEvent(ctx, portID, channelID, upgradeSequence, channelUpgrade)
}

// RestoreChannel restores the given channel to the state prior to upgrade.
func (k Keeper) RestoreChannel(ctx sdk.Context, portID, channelID string, upgradeSequence uint64, err error) error {
errorReceipt := types.NewErrorReceipt(upgradeSequence, err)
k.SetUpgradeErrorReceipt(ctx, portID, channelID, errorReceipt)
currentChannel.State = types.INITUPGRADE

channel, found := k.GetUpgradeRestoreChannel(ctx, portID, channelID)
if !found {
return errorsmod.Wrapf(types.ErrChannelNotFound, "channel-id: %s", channelID)
}
k.SetChannel(ctx, portID, channelID, currentChannel)
k.SetUpgrade(ctx, portID, channelID, upgrade)

k.SetChannel(ctx, portID, channelID, channel)
k.DeleteUpgradeRestoreChannel(ctx, portID, channelID)
k.DeleteUpgradeTimeout(ctx, portID, channelID)

module, _, err := k.LookupModuleByChannel(ctx, portID, channelID)
if err != nil {
return errorsmod.Wrap(err, "could not retrieve module from port-id")
}
k.Logger(ctx).Info("channel state updated", "port-id", portID, "channel-id", channelID, "previous-state", types.OPEN.String(), "new-state", types.INITUPGRADE.String())

portKeeper, ok := k.portKeeper.(*portkeeper.Keeper)
if !ok {
panic("todo: handle this situation")
}
emitChannelUpgradeInitEvent(ctx, portID, channelID, currentChannel, upgrade)
}

cbs, found := portKeeper.Router.GetRoute(module)
// constructProposedUpgrade returns the proposed upgrade from the provided arguments.
func (k Keeper) constructProposedUpgrade(ctx sdk.Context, portID, channelID string, fields types.UpgradeFields, timeout types.UpgradeTimeout) (types.Upgrade, error) {
seq, found := k.GetNextSequenceSend(ctx, portID, channelID)
if !found {
return errorsmod.Wrapf(porttypes.ErrInvalidRoute, "route not found to module: %s", module)
return types.Upgrade{}, types.ErrSequenceSendNotFound
}

return cbs.OnChanUpgradeRestore(ctx, portID, channelID)
return types.Upgrade{
Fields: types.UpgradeFields{
Ordering: fields.Ordering,
ConnectionHops: fields.ConnectionHops,
Version: fields.Version,
},
Timeout: types.UpgradeTimeout{
Height: timeout.Height,
Timestamp: timeout.Timestamp,
},
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
LatestSequenceSend: seq - 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add a comment for why we do the minus 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can add a comment if you think it will help, I think the combination of the names GetNextSequenceSend and LatestSequenceSend make it pretty clear, but I can add a comment to make it extra explicit if you think it makes it more clear.

Copy link
Contributor

Choose a reason for hiding this comment

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

we could also consider adding a new keeper method to sweep it under the rug a little. E.g. k.GetLastSequenceSend(ctx, portID, channelID) which would read the same key/value as GetNextSequenceSend but handle the off by one.

I think if we do that it can be done later in a different PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like @damiannolan's idea!

}, nil
}
Loading