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

Implement Channel Upgrade Try functionality for 04 channel #3333

Closed
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
50e84bc
WIP - adding scaffolding for channel upgrade init
damiannolan Mar 20, 2023
72d2c75
adding application callbacks and no-op implementations
chatton Mar 22, 2023
cef2f6b
Merge branch 'cian/damian/add-noop-application-callbacks' into damian…
damiannolan Mar 22, 2023
ca6bc94
refactoring init upgrade handler and adding tests
damiannolan Mar 22, 2023
c3bc3ae
adding additional testing, response values and events
damiannolan Mar 23, 2023
43eeaab
adding rough impl of subset of ordering method
damiannolan Mar 23, 2023
86753be
adding additional test for upgrade ordering
damiannolan Mar 23, 2023
95b6a2c
Merge branch '04-channel-upgrades' into damian/1618-chan-upgrade-init
damiannolan Mar 23, 2023
21dfc70
adding godocs
damiannolan Mar 23, 2023
524ec5e
updating testing endpoint func to accept timeout args
damiannolan Mar 23, 2023
f1f8e69
satisfy the linter... waaaahh
damiannolan Mar 23, 2023
012d768
cap -> chanCap
damiannolan Mar 23, 2023
186c279
test: adding test for VerifyUpgradeSequenceRecv
chatton Mar 23, 2023
a823c2f
rename test function
chatton Mar 23, 2023
16ced5b
Update modules/core/03-connection/keeper/verify.go
chatton Mar 23, 2023
bde47ac
wip: added most of ChannelUpgradeTry
chatton Mar 23, 2023
ac962e1
adding generic contains method
damiannolan Mar 24, 2023
39fead1
adding upgrade seq to events and response, move version check to vali…
damiannolan Mar 24, 2023
add66ad
Merge branch 'damian/1618-chan-upgrade-init' into cian/issue#1608-add…
damiannolan Mar 24, 2023
903396f
Merge branch 'cian/issue#1608-add-verifychannelupgradesequence-method…
damiannolan Mar 24, 2023
032551f
adding getter/setter for ErrorReceipt, adding tests
damiannolan Mar 24, 2023
0c6cbfb
adding testing scaffolding for chan upgrade try
damiannolan Mar 24, 2023
6287f1b
adding more test coverage
damiannolan Mar 24, 2023
165e23d
use proposed upgrade channel connection, adding test case
damiannolan Mar 24, 2023
f34298d
move empty version checking of proposed channel upgrade to validate b…
damiannolan Mar 24, 2023
9ecf67b
fix port and channel ids passed to verify funcs
damiannolan Mar 24, 2023
5dc87f1
use counterparty ids from upgrade channel
damiannolan Mar 24, 2023
474a7d8
adding upgrade sequence return value, inline comments and some refactor
damiannolan Mar 24, 2023
e56607f
adding restore channel scaffolding
damiannolan Mar 24, 2023
30d076c
adding write func for successful upgrade try
damiannolan Mar 24, 2023
18e81a8
discard redundant proofHeights in test endpoint
damiannolan Mar 24, 2023
1696a70
commit error receipt to state and abort channel upgrade early
damiannolan Mar 24, 2023
4c51388
ensure upgraded channel connection end is relative to counterparty
damiannolan Mar 27, 2023
e797812
merge with feature branch and add calls to verifiy and restore functions
chatton Mar 29, 2023
e5fd5d6
updated error message
chatton Mar 29, 2023
eed9b91
added previous version as return argument
chatton Mar 29, 2023
f1b19ba
re-added tests for UpgradeTry
chatton Mar 29, 2023
c5cf6e6
wip: adding test for crossing hellos
chatton Mar 29, 2023
9b750c1
refactor chan upgrade try to accomodate crossing hellos, adding test …
damiannolan Mar 30, 2023
1dde2ea
adding additional test cases for connection not found, invalid counte…
damiannolan Mar 30, 2023
3f4205e
adding more tests and timeout checks commented out
damiannolan Mar 30, 2023
fcc0c34
cleanup
damiannolan Mar 30, 2023
0de9d39
appeased the linting overlords
chatton Mar 30, 2023
564ddec
adding timeout tests
chatton Mar 30, 2023
3124a14
cleaning up some testcases
damiannolan Mar 31, 2023
ba9b2c4
adding happy path response values
damiannolan Mar 31, 2023
ccced69
adding upgrade try events
damiannolan Mar 31, 2023
438ea63
cleanup errors file
damiannolan Mar 31, 2023
7f7c399
Merge branch '04-channel-upgrades' into cian/issue#1619-implement-cha…
damiannolan Mar 31, 2023
ddf179d
removing check that is already in validate basic
damiannolan Mar 31, 2023
900e632
merge 04-channel-upgrades
chatton Apr 3, 2023
8ddc924
update with spec changes
charleenfei Apr 3, 2023
0d49837
update upgrade sequence code blockk
charleenfei Apr 3, 2023
8fcca78
created separate function to return the connection end
chatton Apr 3, 2023
1886a50
update missing test coverage
charleenfei Apr 3, 2023
ea1d7dc
fix linting issues
chatton Apr 4, 2023
9352b30
Merge branch 'separate-out-happypath-crossing-hellos' into cian/issue…
chatton Apr 4, 2023
e389c77
adding additional test cases and assigning fields to existing channel
chatton Apr 4, 2023
4839088
added check to verify the computed upgrade channel equals the expecte…
chatton Apr 4, 2023
05a299f
todos
charleenfei Apr 4, 2023
4eea882
mv event emission to other pr
charleenfei Apr 4, 2023
004162b
fix linter
chatton Apr 5, 2023
7df0e52
small error message change
crodriguezvega Apr 5, 2023
35c7e35
remove callback word from log messages
crodriguezvega Apr 5, 2023
64ef5aa
add log message on failed upgrade try callback
crodriguezvega Apr 5, 2023
abe38fe
addressing PR feedback
chatton Apr 5, 2023
01b883f
Merge branch '04-channel-upgrades' into cian/issue#1619-implement-cha…
chatton Apr 5, 2023
bc145f9
rm deep equal check in favour of zeroing custom fields
charleenfei Apr 5, 2023
dab7eb1
update test coverage to test for channel state
charleenfei Apr 5, 2023
163cd9a
lint
charleenfei Apr 5, 2023
6547783
pr review updates
charleenfei Apr 5, 2023
f445158
precommit
charleenfei Apr 5, 2023
5874333
rm references to chanCap
charleenfei Apr 5, 2023
00a3136
update
charleenfei Apr 5, 2023
c158a98
addressing PR feedback
chatton Apr 6, 2023
d6660b0
remove restore call from timeout checks
chatton Apr 6, 2023
b79cdfc
Merge branch '04-channel-upgrades' into channel-upgrade-try
charleenfei Apr 11, 2023
946d3c5
Merge branch 'cian/issue#1619-implement-channelupgradetry-functionali…
charleenfei Apr 11, 2023
61427c0
merge conflicts
charleenfei Apr 11, 2023
bd8574c
Merge branch 'cian/issue#1619-implement-channelupgradetry-functionali…
chatton Apr 11, 2023
f9d0ee6
Merge branch '04-channel-upgrades' into cian/issue#1619-implement-cha…
chatton Apr 11, 2023
a3603f8
Merge branch 'cian/issue#1619-implement-channelupgradetry-functionali…
chatton Apr 11, 2023
77848dd
fixing tests for init and try
chatton Apr 12, 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
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,7 @@ func (im IBCMiddleware) OnChanUpgradeConfirm(ctx sdk.Context, portID, channelID
}

// OnChanUpgradeRestore implements the IBCModule interface
func (im IBCMiddleware) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) error {
return nil
func (im IBCMiddleware) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) {
}

// SendPacket implements the ICS4 Wrapper interface
Expand Down
3 changes: 1 addition & 2 deletions modules/apps/27-interchain-accounts/host/ibc_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,5 @@ func (im IBCModule) OnChanUpgradeConfirm(ctx sdk.Context, portID, channelID stri
}

// OnChanUpgradeRestore implements the IBCModule interface
func (im IBCModule) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) error {
return nil
func (im IBCModule) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) {
}
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 @@ -341,8 +341,8 @@ func (im IBCMiddleware) OnChanUpgradeConfirm(ctx sdk.Context, portID, channelID
}

// OnChanUpgradeRestore implements the IBCModule interface
func (im IBCMiddleware) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) error {
return im.app.OnChanUpgradeRestore(ctx, portID, channelID)
func (im IBCMiddleware) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) {
im.app.OnChanUpgradeRestore(ctx, portID, channelID)
}

// SendPacket implements the ICS4 Wrapper interface
Expand Down
3 changes: 1 addition & 2 deletions modules/apps/transfer/ibc_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,5 @@ func (im IBCModule) OnChanUpgradeConfirm(ctx sdk.Context, portID, channelID stri
}

// OnChanUpgradeRestore implements the IBCModule interface
func (im IBCModule) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) error {
return nil
func (im IBCModule) OnChanUpgradeRestore(ctx sdk.Context, portID, channelID string) {
}
20 changes: 20 additions & 0 deletions modules/core/04-channel/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,23 @@ func emitChannelUpgradeInitEvent(ctx sdk.Context, portID string, channelID strin
),
})
}

// emitChannelUpgradeTryEvent emits a channel upgrade try event
func emitChannelUpgradeTryEvent(ctx sdk.Context, portID string, channelID string, upgradeSequence uint64, channel types.Channel) {
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeChannelUpgradeTry,
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)),
Comment on lines +300 to +307
Copy link
Contributor

Choose a reason for hiding this comment

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

we should emit the channel order as well since it can change. Looks like it is missing in Init as well

),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
),
})
}
268 changes: 264 additions & 4 deletions modules/core/04-channel/keeper/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"

"github.com/cosmos/ibc-go/v7/internal/collections"
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"
"github.com/cosmos/ibc-go/v7/modules/core/exported"
)

// ChanUpgradeInit is called by a module to initiate a channel upgrade handshake with
Expand Down Expand Up @@ -86,17 +88,249 @@ func (k Keeper) WriteUpgradeInitChannel(
channelID string,
upgradeSequence uint64,
channelUpgrade types.Channel,
) {
) error {
defer telemetry.IncrCounter(1, "ibc", "channel", "upgrade-init")

channel, found := k.GetChannel(ctx, portID, channelID)
if !found {
return errorsmod.Wrapf(types.ErrChannelNotFound, "failed to retrieve channel %s on port %s", channelID, portID)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should panic in my opinion. Then we can remove the error on this function. Write/Set functions should either succeed or panic

}

// assign directly the fields that are modifiable.
// counterparty fields may not be changed.
channel.Version = channelUpgrade.Version
channel.Ordering = channelUpgrade.Ordering
channel.ConnectionHops = channelUpgrade.ConnectionHops

// prevent silent failures if fields that are not allowed to be modified are changed in the upgrade channel
// request.
if !reflect.DeepEqual(channel, channelUpgrade) {
return errorsmod.Wrapf(types.ErrInvalidChannel, "expected: %v but got: %v", channel, channelUpgrade)
}

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)
return nil
}

// ChanUpgradeTry is called by a module to accept the first step of a channel upgrade
// handshake initiated by a module on another chain.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it would be good to add something here in the good about the upgrade sequence that is returned, because if this function returns a not nil error, then upgradeSequence will be zero, but in fact the upgrade sequence stored could be different.

func (k Keeper) ChanUpgradeTry(
ctx sdk.Context,
portID string,
channelID string,
chanCap *capabilitytypes.Capability,
counterpartyChannel types.Channel,
Copy link
Contributor

Choose a reason for hiding this comment

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

counterpartyUpgradeChannel?

counterpartyUpgradeSequence uint64,
proposedUpgradeChannel types.Channel,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
proofChannel []byte,
proofUpgradeTimeout []byte,
proofUpgradeSequence []byte,
proofHeight clienttypes.Height,
) (upgradeSequence uint64, previousVersion string, err error) {
channel, found := k.GetChannel(ctx, portID, channelID)
if !found {
return 0, "", errorsmod.Wrapf(types.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}

if !collections.Contains(channel.State, []types.State{types.OPEN, types.INITUPGRADE}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks really nice. Should we add a comment indicating that this is accounting for crossing hellos?

return 0, "", errorsmod.Wrapf(types.ErrInvalidChannelState, "expected one of [%s, %s], got %s", types.OPEN, types.INITUPGRADE, 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)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

note: I'm fairly certain this is unnecessary, see spec issue. I would be so happy to see these unnecessary checks removed 😄


if proposedUpgradeChannel.Counterparty.PortId != channel.Counterparty.PortId ||
proposedUpgradeChannel.Counterparty.ChannelId != channel.Counterparty.ChannelId {
return 0, "", errorsmod.Wrap(types.ErrInvalidChannel, "proposed channel upgrade is invalid")
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 use same error message as in INIT?

}

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

Choose a reason for hiding this comment

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

what do we think about adding a validateProposedUpgradeChannelFields function? We could try to reuse it for upgradeInit

Copy link
Contributor

Choose a reason for hiding this comment

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

this is a good idea, we will update in a followup pr along with other util methods/small updates (see PR description)


if counterpartyChannel.Ordering != proposedUpgradeChannel.Ordering {
return 0, "", errorsmod.Wrapf(types.ErrInvalidChannelOrdering, "channel ordering of counterparty channel and proposed channel must be equal")
}

connectionEnd, err := k.getUpgradeTryConnectionEnd(ctx, portID, channelID, channel)
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks great. What about k.getConnectionForVerification? We will need to use this function for upgradeAck. The general idea is that during an upgrade, the existing connection needs to be used for verification until the upgrade succeeds (ie proposed channel upgrade fields must not be used until the upgrade succeeds)

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 already ran into this problem looking into upgrade ack! The only thing that changes is the condition for a crossing hello

Copy link
Contributor

Choose a reason for hiding this comment

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

we will make a generic function for this in a followup pr

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(),
)
}
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 add a helper function. We could do this as a pr to main to cleanup other code as well

if !connectionEnd.IsOpen() {


if connectionEnd.GetCounterparty().GetConnectionID() != counterpartyChannel.ConnectionHops[0] {
Copy link
Contributor

Choose a reason for hiding this comment

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

we should add a comment like the spec changes have. This is a tricky check to understand

Copy link
Contributor

Choose a reason for hiding this comment

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

which spec changes are you referring to?

return 0, "", errorsmod.Wrapf(connectiontypes.ErrInvalidConnection, "unexpected counterparty channel connection hops, expected %s but got %s", connectionEnd.GetCounterparty().GetConnectionID(), counterpartyChannel.ConnectionHops[0])
}

if err := k.connectionKeeper.VerifyChannelState(ctx, connectionEnd, proofHeight, proofChannel, proposedUpgradeChannel.Counterparty.PortId,
Copy link
Contributor

Choose a reason for hiding this comment

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

So we are verifying 3 things:

  • channel
  • sequence
  • timeout

From my understanding, there is no particular reason that sequence and timeout are separated within the provable store (they could be under the same upgrade type). The channel is under its' own path for no strong reason, but one benefit is that you can be certain the counterparty has paused packet processing. There could be a construction where all 3 are under the same provable upgrade type:

interface Upgrade {
    proposedChannel Channel
    sequence uint64
    timeout Timeout
}

We could add a k.connectionKeeper.VerifyUpgrade which does all 3, but I don't have a strong preference, just mentioning as food for thought. The main downside of such a function is passing 3 proofs/values at once. The argument list would be very large:

(ctx, connectionEnd, channel.Counterparty.PortId, channel.Counterparty.ChannelId, proofHeight, proofChannel, counterpartyChannel, proofUpgradeTimeout, upgradeTimeout, proofUpgradeSequence, upgradeSequence

After seeing that argument list, I think I actually prefer doing the split here

proposedUpgradeChannel.Counterparty.ChannelId, counterpartyChannel); 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.

Counterparty fields shouldn't change, lets use the existing channel end

return 0, "", err
}

upgradeTimeout := types.UpgradeTimeout{TimeoutHeight: timeoutHeight, TimeoutTimestamp: timeoutTimestamp}
if err := k.connectionKeeper.VerifyChannelUpgradeTimeout(ctx, connectionEnd, proofHeight, proofUpgradeTimeout, proposedUpgradeChannel.Counterparty.PortId,
proposedUpgradeChannel.Counterparty.ChannelId, upgradeTimeout); err != nil {
return 0, "", err
}

if err := k.connectionKeeper.VerifyChannelUpgradeSequence(ctx, connectionEnd, proofHeight, proofUpgradeSequence, proposedUpgradeChannel.Counterparty.PortId,
proposedUpgradeChannel.Counterparty.ChannelId, counterpartyUpgradeSequence); 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.

ditto on Counterparty.PortId

return 0, "", err
}
Copy link
Member

@damiannolan damiannolan Mar 31, 2023

Choose a reason for hiding this comment

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

We should be doing state verification of counterparty using the original channel connection. This is currently using the connection looked up from the proposed upgrade channel connection hops.

This might look a little ugly as depending on whether or not we are in a crossing hello then we'll need to retrieve the connection from the current channel (not crossing hello, state: OPEN) or the restore channel (crossing hello, state: INITUPGRADE).

ref: cosmos/ibc#950 (comment)

cc. @AdityaSripal

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is now being handled in the above getUpgradeTryConnectionEnd function which handles the crossing hellos case.


// check if upgrade timed out by comparing it with the latest height of the chain
selfHeight := clienttypes.GetSelfHeight(ctx)
if !timeoutHeight.IsZero() && selfHeight.GTE(timeoutHeight) {
if err := k.RestoreChannelAndWriteErrorReceipt(ctx, portID, channelID, upgradeSequence, types.ErrUpgradeTimeout); 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.

Restoring the channel and writing a receipt here would only be needed if the channel state is in INITUPGRADE, right? (crossing hello). Otherwise, just aborting the transaction and calling timeout on the counterparty would be enough, right? I think the same applies for the timestamp check below.

Copy link
Contributor Author

@chatton chatton Apr 11, 2023

Choose a reason for hiding this comment

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

yes you're right, this actually ended up happening in #3418 which is probably not where that change should have been introduced 😅

return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, err.Error())
}
return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, "upgrade timed out")
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 have a similar error message as for receive packet?

}

// check if upgrade timed out by comparing it with the latest timestamp of the chain
if timeoutTimestamp != 0 && uint64(ctx.BlockTime().UnixNano()) >= timeoutTimestamp {
Copy link
Member

Choose a reason for hiding this comment

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

Should we move this check up and just abort the tx, the relayer can call timout at this point if needed.

However, we get into an awkward situation if we are also at UPGRADE_INIT, then we would have to wait for timeout on the other side as well

if err := k.RestoreChannelAndWriteErrorReceipt(ctx, portID, channelID, upgradeSequence, types.ErrUpgradeTimeout); err != nil {
return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, err.Error())
}
return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, "upgrade timed out")
Copy link
Contributor

Choose a reason for hiding this comment

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

}

switch channel.State {
case types.OPEN:
upgradeSequence = uint64(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be 1?

Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't it actually still be 0 because we are in channel state OPEN? we update the sequence further down the code block

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

// if the counterparty upgrade sequence is ahead then fast forward so both channel ends are using the same sequence for the current upgrade
if counterpartyUpgradeSequence > upgradeSequence {
upgradeSequence = counterpartyUpgradeSequence
k.SetUpgradeSequence(ctx, portID, channelID, upgradeSequence)
} else {
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not smaller than the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
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
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not smaller than the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "counterparty chain upgrade sequence <= upgrade sequence (%d <= %d)", counterpartyUpgradeSequence, upgradeSequence))

// the upgrade sequence is incremented so both sides start the next upgrade with a fresh sequence.
upgradeSequence++

k.SetUpgradeErrorReceipt(ctx, portID, channelID, errorReceipt)
k.SetUpgradeSequence(ctx, portID, channelID, upgradeSequence)

// TODO: emit error receipt events
Copy link
Member

Choose a reason for hiding this comment

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

I don't mind doing error receipt events in a follow up PR, considering this is already quite large


// do we want to return upgrade sequence here to include in response??
return 0, "", errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade aborted, error receipt written for upgrade sequence: %d", errorReceipt.GetSequence())
}

// this is first message in upgrade handshake on this chain so we must store original channel in restore channel path
// in case we need to restore channel later.
k.SetUpgradeRestoreChannel(ctx, portID, channelID, channel)
case types.INITUPGRADE:
upgradeSequence, found = k.GetUpgradeSequence(ctx, portID, channelID)
if !found {
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not smaller than the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
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
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not smaller than the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrap(types.ErrUpgradeAborted, "upgrade sequence not found")

k.SetUpgradeErrorReceipt(ctx, portID, channelID, errorReceipt)

return 0, "", errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade aborted, error receipt written for upgrade sequence: %d", upgradeSequence)
}

if upgradeSequence != counterpartyUpgradeSequence {
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not equal to the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
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
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence %d was not equal to the counter party chain upgrade sequence %d", upgradeSequence, counterpartyUpgradeSequence))
errorReceipt := types.NewErrorReceipt(upgradeSequence, errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade sequence ≠ counterparty chain upgrade sequence (%d ≠ %d)", upgradeSequence, counterpartyUpgradeSequence))


// set to the max of the two
if counterpartyUpgradeSequence > upgradeSequence {
upgradeSequence = counterpartyUpgradeSequence
k.SetUpgradeSequence(ctx, portID, channelID, upgradeSequence)
}

k.SetUpgradeErrorReceipt(ctx, portID, channelID, errorReceipt)
return 0, "", errorsmod.Wrapf(types.ErrUpgradeAborted, "upgrade aborted, error receipt written for upgrade sequence: %d", upgradeSequence)
Copy link
Contributor

Choose a reason for hiding this comment

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

The upgrade sequence mentioned in this error message could be different than the one in the error receipt if it gets updated in the if clause just above.

}

// if there is a crossing hello, i.e an UpgradeInit has been called on both channelEnds,
// then we must ensure that the proposedUpgrade by the counterparty is the same as the currentChannel
// except for the channel state (upgrade channel will be in TRYUPGRADE and current channel will be in INITUPGRADE)
// if the proposed upgrades on either side are incompatible, then we will restore the channel and cancel the upgrade.
channel.State = types.TRYUPGRADE
k.SetChannel(ctx, portID, channelID, channel)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this SetChannel (maybe for clarity) be done at the end?

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 updated channel state is used just a few lines down and I think separating them out makes things a little harder to follow in this case

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but to use the channel state you don't need it stored, right? For me it's just that I think it would be more clear if you set the channel when all the checks have passed (like in upgrade init). I know that if there's an error, the state changes will be reverted, but feels like the code would be more clear if it's at the end, instead of in between this complicated logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

I tend to agree with @chatton that it is clearer to keep the logic together, rather than having the channel state being updated in one place and then actually set further down with the potential restore channel logic happening in between


if !reflect.DeepEqual(channel, proposedUpgradeChannel) {
// TODO: log and emit events
if err := k.RestoreChannelAndWriteErrorReceipt(ctx, portID, channelID, upgradeSequence, types.ErrInvalidChannel); err != nil {
return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, err.Error())
}
return 0, "", errorsmod.Wrap(types.ErrUpgradeAborted, "proposed upgrade channel did not equal expected channel")
}

// retrieve restore channel to return previous version
restoreChannel, found := k.GetUpgradeRestoreChannel(ctx, portID, channelID)
if !found {
//(this case should be unreachable): write error receipt for upgrade sequence and abort / cancel upgrade
errorReceipt := types.NewErrorReceipt(upgradeSequence, err)
k.SetUpgradeErrorReceipt(ctx, portID, channelID, errorReceipt)
return 0, "", errorsmod.Wrapf(types.ErrChannelNotFound, "upgrade aborted, error receipt written for upgrade sequence: %d", upgradeSequence)
}

return upgradeSequence, restoreChannel.Version, nil
default:
return 0, "", errorsmod.Wrapf(types.ErrInvalidChannelState, "expected one of [%s, %s] but got %s", types.OPEN, types.INITUPGRADE, channel.State)
}

return upgradeSequence, channel.Version, nil
}

// 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 {
// WriteUpgradeTryChannel writes a channel which has successfully passed the UpgradeTry handshake step.
// An event is emitted for the handshake step.
func (k Keeper) WriteUpgradeTryChannel(
ctx sdk.Context,
portID,
channelID,
proposedUpgradeVersion string,
upgradeSequence uint64,
channelUpgrade types.Channel,
Copy link
Contributor

@colin-axner colin-axner Apr 3, 2023

Choose a reason for hiding this comment

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

see #3401. I know it isn't clean/optimized, but I actually think the channel end should be retrieved from state instead of writing the proposed upgrade channel. This comes from a general security principal that you should be recreating the data you want to write to disk rather than writing the user input (as in this case it is unsafe without an additional check). This is partially why we do this for openAck and openConfirm. I'm open to differing opinions, but it would be nice to be defensive here, if another field is added to the channel end which cannot be modified in an upgrade, we would need to remember to add another defensive check

Copy link
Contributor

@colin-axner colin-axner Apr 3, 2023

Choose a reason for hiding this comment

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

What if we add proposedChannelUpgradeInfo or some similar type which contains all the fields that can be modified. This can be passed as an argument to MsgChannelUpgradeInit, ChanUpgradeInit, OnChanUpgradeInit and WriteChannelUpgradeInit. It seems redundant to check that the proposedUpgradeChannel hasn't modified certain fields, seems safer to simply exclude them from consideration by adding an extra type

Copy link
Contributor

Choose a reason for hiding this comment

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

see @chatton's suggestion here

Copy link
Contributor Author

@chatton chatton Apr 4, 2023

Choose a reason for hiding this comment

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

nice catch, but do we need to create a separate type for this? We can fetch the existing channel and then modify it based on values present in channelUpgrade. A proposedChannelUpgradeInfo would surely be passed with the same fields. I do think it's a good idea to not set the channel directly 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.

The benefit of a separate type I can think of is that not all fields are upgradable, and so some might end up being ignored. With a custom type we can provide only fields that are valid to upgrade.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes exactly. The benefit of that is that there is less room for error if the submitter specifies the non-modifiable fields incorrectly. We could use a similar syntax to client state ZeroCustomFields() function which returns a client state with the custom fields zeroed out (except we would do the opposite). I'm happy to long as we aren't setting the proposedUpgradeChannel directly into state and are simply modifying the existing one

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 a helper like that is the best option, this PR is getting a little unwieldy so I think I will create a follow up for this if you're okay with it!

) error {
defer telemetry.IncrCounter(1, "ibc", "channel", "upgrade-try")

channel, found := k.GetChannel(ctx, portID, channelID)
if !found {
return errorsmod.Wrapf(types.ErrChannelNotFound, "failed to retrieve channel %s on port %s", channelID, portID)
}

// assign directly the fields that are modifiable.
// counterparty fields may not be changed.
channel.Version = proposedUpgradeVersion
channel.Ordering = channelUpgrade.Ordering
channel.ConnectionHops = channelUpgrade.ConnectionHops
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this code updating the channel fields is duplicated in WriteUpgradeInitChannel. Maybe we can have a method on the channel end struct to update all upgradable fields?

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 introduce a helper function 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.

Yes, more than happy! :)


// prevent silent failures if fields that are not allowed to be modified are changed in the upgrade channel
// request.
if !reflect.DeepEqual(channel, channelUpgrade) {
return errorsmod.Wrapf(types.ErrInvalidChannel, "expected: %v but got: %v", channel, channelUpgrade)
}

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

// TODO: previous state will not be OPEN in the case of crossing hellos. Determine this state correctly.
k.Logger(ctx).Info("channel state updated", "port-id", portID, "channel-id", channelID, "previous-state", types.OPEN.String(), "new-state", types.TRYUPGRADE.String())
Copy link
Member

Choose a reason for hiding this comment

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

minor nit: in crossing hellos previous state would be INITUPGRADE


emitChannelUpgradeTryEvent(ctx, portID, channelID, upgradeSequence, channelUpgrade)
return nil
}

// TODO: should we pull out the error receipt logic from this function? They seem like two discrete operations.

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

Expand Down Expand Up @@ -124,5 +358,31 @@ func (k Keeper) RestoreChannel(ctx sdk.Context, portID, channelID string, upgrad
return errorsmod.Wrapf(porttypes.ErrInvalidRoute, "route not found to module: %s", module)
}

return cbs.OnChanUpgradeRestore(ctx, portID, channelID)
cbs.OnChanUpgradeRestore(ctx, portID, channelID)
return nil
}

// getUpgradeTryConnectionEnd returns the connection end that should be used. During crossing hellos, the restore
// channel connection end is used, while in a regular flow the current channel connection end is used.
func (k Keeper) getUpgradeTryConnectionEnd(ctx sdk.Context, portID string, channelID string, currentChannel types.Channel) (exported.ConnectionI, error) {
isCrossingHellos := currentChannel.State == types.INITUPGRADE
if isCrossingHellos {
// fetch restore channel
restoreChannel, found := k.GetUpgradeRestoreChannel(ctx, portID, channelID)
if !found {
return nil, errorsmod.Wrapf(types.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}
connectionEnd, err := k.GetConnection(ctx, restoreChannel.ConnectionHops[0])
if err != nil {
return nil, err
}
return connectionEnd, nil
}

// use current channel
connectionEnd, err := k.GetConnection(ctx, currentChannel.ConnectionHops[0])
if err != nil {
return nil, err
}
return connectionEnd, nil
}
Loading