-
Notifications
You must be signed in to change notification settings - Fork 635
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
startFlushUpgradeHandshake #3671
Changes from 14 commits
a293750
6e404c9
7eb8b6e
4355431
f212a81
bf97d38
3209e57
cfe4502
a04108f
7f8130d
c03258a
cf86f93
b73ab6a
9adbb34
fb320aa
f242385
85df989
d3029e4
fa9e447
44b28e6
d46665c
270f067
16f0631
32a7865
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import ( | |
"reflect" | ||
|
||
errorsmod "cosmossdk.io/errors" | ||
sdkmath "cosmossdk.io/math" | ||
"github.com/cosmos/cosmos-sdk/telemetry" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
|
||
|
@@ -109,6 +110,110 @@ func (k Keeper) WriteUpgradeTryChannel( | |
emitChannelUpgradeTryEvent(ctx, portID, channelID, channel, proposedUpgrade) | ||
} | ||
|
||
// startFlushUpgradeHandshake will verify the counterparty proposed upgrade and the current channel state. | ||
// Once the counterparty information has been verified, it will be validated against the self proposed upgrade. | ||
// If any of the proposed upgrade fields are incompatible, an upgrade error will be returned resulting in an | ||
// aborted upgrade. | ||
// | ||
//lint:ignore U1000 Ignore unused function temporarily for debugging | ||
func (k Keeper) startFlushUpgradeHandshake( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding is that the writes that happen in the spec will happen in a separate function in the implementation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. they will happen in the |
||
ctx sdk.Context, | ||
portID, | ||
channelID string, | ||
proposedUpgradeFields types.UpgradeFields, | ||
counterpartyChannel types.Channel, | ||
counterpartyUpgrade types.Upgrade, | ||
proofCounterpartyChannel, | ||
proofCounterpartyUpgrade []byte, | ||
proofHeight clienttypes.Height, | ||
) error { | ||
channel, found := k.GetChannel(ctx, portID, channelID) | ||
if !found { | ||
return errorsmod.Wrapf(types.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID) | ||
} | ||
|
||
connection, err := k.GetConnection(ctx, channel.ConnectionHops[0]) | ||
if err != nil { | ||
return errorsmod.Wrap(err, "failed to retrieve connection using the channel connection hops") | ||
} | ||
|
||
if connection.GetState() != int32(connectiontypes.OPEN) { | ||
return errorsmod.Wrapf(connectiontypes.ErrInvalidConnectionState, "connection state is not OPEN (got %s)", connectiontypes.State(connection.GetState()).String()) | ||
} | ||
|
||
// verify the counterparty channel state containing the upgrade sequence | ||
if err := k.connectionKeeper.VerifyChannelState( | ||
ctx, | ||
connection, | ||
proofHeight, proofCounterpartyChannel, | ||
channel.Counterparty.PortId, | ||
channel.Counterparty.ChannelId, | ||
counterpartyChannel, | ||
); err != nil { | ||
return errorsmod.Wrap(err, "failed to verify counterparty channel state") | ||
} | ||
|
||
// verifies the proof that a particular proposed upgrade has been stored in the upgrade path of the counterparty | ||
if err := k.connectionKeeper.VerifyChannelUpgrade( | ||
ctx, | ||
connection, | ||
proofHeight, proofCounterpartyUpgrade, | ||
channel.Counterparty.PortId, | ||
channel.Counterparty.ChannelId, | ||
counterpartyUpgrade, | ||
); err != nil { | ||
return errorsmod.Wrap(err, "failed to verify counterparty upgrade") | ||
} | ||
|
||
// the current upgrade handshake must only continue if both channels are using the same upgrade sequence, | ||
// otherwise an error receipt must be written so that the upgrade handshake may be attempted again with synchronized sequences | ||
if counterpartyChannel.UpgradeSequence != channel.UpgradeSequence { | ||
// save the previous upgrade sequence for the error message | ||
prevUpgradeSequence := channel.UpgradeSequence | ||
|
||
// error on the higher sequence so that both chains synchronize on a fresh sequence | ||
channel.UpgradeSequence = sdkmath.Max(counterpartyChannel.UpgradeSequence, channel.UpgradeSequence) | ||
k.SetChannel(ctx, portID, channelID, channel) | ||
|
||
return types.NewUpgradeError(channel.UpgradeSequence, errorsmod.Wrapf( | ||
types.ErrIncompatibleCounterpartyUpgrade, "expected upgrade sequence (%d) to match counterparty upgrade sequence (%d)", prevUpgradeSequence, counterpartyChannel.UpgradeSequence), | ||
) | ||
} | ||
|
||
// assert that both sides propose the same channel ordering | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we think we might need to add more checks in the future, we could consider making a helper function for all the below checks to validate the counterparty upgrade against the self upgrade, maybe something like: err := k.checkUpgradesForCompatibility(proposedUpgrade, counterpartyUpgrade)
if err != nil {
return types.NewUpgradeError(channel.UpgradeSequence, err)
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this suggestion a lot, this also adds a clear location for where all checks can go, and also clarifies the purpose of certain checks a lot more. |
||
if proposedUpgradeFields.Ordering != counterpartyUpgrade.Fields.Ordering { | ||
return types.NewUpgradeError(channel.UpgradeSequence, errorsmod.Wrapf( | ||
types.ErrIncompatibleCounterpartyUpgrade, "expected upgrade ordering (%s) to match counterparty upgrade ordering (%d)", proposedUpgradeFields.Ordering, counterpartyUpgrade.Fields.Ordering), | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
} | ||
|
||
proposedConnection, err := k.GetConnection(ctx, proposedUpgradeFields.ConnectionHops[0]) | ||
if err != nil { | ||
// NOTE: this error is expected to be unreachable as the proposed upgrade connectionID should have been | ||
// validated in the upgrade INIT and TRY handlers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason to validate in both places? The spec seems to be doing the validation only in this function and not in the handlers, right? If we keep validation in the handlers then behaviour of the implementation deviates from the spec because the implementation will fail the transaction, but not write the error receipt, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to use the proposedConnection in the handlers to construct the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec does this check twice: // initUpgradeHandshake
...
// proposedConnection must exist and be in OPEN state for
// channel upgrade to be accepted
proposedConnection = provableStore.Get(connectionPath(proposedUpgradeFields.connectionHops[0])
abortTransactionUnless(proposedConnection != null && proposedConnection.state == OPEN) and // startFlushUpgradeHandshake
...
// connectionHops can change in a channelUpgrade, however both sides must still be each other's counterparty.
proposedConnection = provableStore.get(connectionPath(proposedUpgradeFields.connectionHops[0])
if (proposedConnection == null || proposedConnection.state != OPEN) {
restoreChannel(portIdentifier, channelIdentifier)
} |
||
return types.NewUpgradeError(channel.UpgradeSequence, errorsmod.Wrap( | ||
err, "expected proposed connection to be found"), | ||
) | ||
} | ||
|
||
if proposedConnection.GetState() != int32(connectiontypes.OPEN) { | ||
// NOTE: this error is expected to be unreachable as the proposed upgrade connectionID should have been | ||
// validated in the upgrade INIT and TRY handlers | ||
return types.NewUpgradeError(channel.UpgradeSequence, errorsmod.Wrapf( | ||
connectiontypes.ErrInvalidConnectionState, "expected proposed connection to be OPEN (got %s)", connectiontypes.State(proposedConnection.GetState()).String()), | ||
) | ||
} | ||
|
||
// connectionHops can change in a channelUpgrade, however both sides must still be each other's counterparty. | ||
if counterpartyUpgrade.Fields.ConnectionHops[0] != proposedConnection.GetCounterparty().GetConnectionID() { | ||
return types.NewUpgradeError(channel.UpgradeSequence, errorsmod.Wrapf( | ||
types.ErrIncompatibleCounterpartyUpgrade, "counterparty upgrade connection end is not a counterparty of self proposed connection end (%s != %s)", counterpartyUpgrade.Fields.ConnectionHops[0], proposedConnection.GetCounterparty().GetConnectionID()), | ||
) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// validateUpgradeFields 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,11 @@ package keeper_test | |
import ( | ||
"fmt" | ||
|
||
errorsmod "cosmossdk.io/errors" | ||
|
||
connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" | ||
"github.com/cosmos/ibc-go/v7/modules/core/04-channel/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" | ||
|
@@ -126,6 +129,185 @@ func (suite *KeeperTestSuite) TestChanUpgradeInit() { | |
} | ||
} | ||
|
||
// UpgradeInit will be run on chainA and startFlushUpgradeHandshake | ||
// will be called on chainB | ||
Comment on lines
+133
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] if we're adding a docstring, we should make sure it starts with the name of the function // TestStartFlushUpgradeHandshake tests the startFlushUpgradeHandshake.
// UpgradeInit will be run on chainA and startFlushUpgradeHandshake
// will be called on chainB |
||
func (suite *KeeperTestSuite) TestStartFlushUpgradeHandshake() { | ||
var ( | ||
path *ibctesting.Path | ||
upgrade types.Upgrade | ||
counterpartyChannel types.Channel | ||
counterpartyUpgrade types.Upgrade | ||
) | ||
|
||
testCases := []struct { | ||
name string | ||
malleate func() | ||
// TODO: currently the code asserts based on an expected sdk error | ||
// Allow support for asserting either the sdk error type or that the error is an upgrade error | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
expError error | ||
}{ | ||
{ | ||
"success", | ||
func() {}, | ||
nil, | ||
}, | ||
{ | ||
"channel not found", | ||
func() { | ||
path.EndpointB.ChannelID = "invalid-channel" | ||
path.EndpointB.ChannelConfig.PortID = "invalid-port" | ||
}, | ||
types.ErrChannelNotFound, | ||
}, | ||
{ | ||
"connection not found", | ||
func() { | ||
channel := path.EndpointB.GetChannel() | ||
channel.ConnectionHops[0] = "invalid-connection" | ||
path.EndpointB.SetChannel(channel) | ||
}, | ||
connectiontypes.ErrConnectionNotFound, | ||
}, | ||
{ | ||
"connection state is not in OPEN state", | ||
func() { | ||
conn := path.EndpointB.GetConnection() | ||
conn.State = connectiontypes.INIT | ||
path.EndpointB.SetConnection(conn) | ||
}, | ||
connectiontypes.ErrInvalidConnectionState, | ||
}, | ||
{ | ||
"failed verification for counterparty channel state due to incorrectly constructed counterparty channel", | ||
func() { | ||
counterpartyChannel.State = types.CLOSED | ||
}, | ||
commitmenttypes.ErrInvalidProof, | ||
}, | ||
{ | ||
"failed verification for counterparty upgrade due to incorrectly constructed counterparty upgrade", | ||
func() { | ||
counterpartyUpgrade.LatestSequenceSend = 100 | ||
}, | ||
commitmenttypes.ErrInvalidProof, | ||
}, | ||
{ | ||
"upgrade sequence mismatch, endpointB channel upgrade sequence is ahead", | ||
func() { | ||
channel := path.EndpointB.GetChannel() | ||
channel.UpgradeSequence++ | ||
path.EndpointB.SetChannel(channel) | ||
}, | ||
types.ErrIncompatibleCounterpartyUpgrade, | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
{ | ||
"upgrade ordering is not the same on both sides", | ||
func() { | ||
upgrade.Fields.Ordering = types.ORDERED | ||
}, | ||
types.ErrIncompatibleCounterpartyUpgrade, | ||
}, | ||
{ | ||
"proposed connection is not found", | ||
func() { | ||
upgrade.Fields.ConnectionHops[0] = "invalid-connection" | ||
}, | ||
connectiontypes.ErrConnectionNotFound, | ||
}, | ||
{ | ||
"proposed connection is not in OPEN state", | ||
func() { | ||
// reuse existing connection to create a new connection in a non OPEN state | ||
connectionEnd := path.EndpointB.GetConnection() | ||
connectionEnd.State = connectiontypes.UNINITIALIZED | ||
connectionEnd.Counterparty.ConnectionId = counterpartyUpgrade.Fields.ConnectionHops[0] // both sides must be each other's counterparty | ||
|
||
// set proposed connection in state | ||
proposedConnectionID := "connection-100" | ||
suite.chainB.GetSimApp().GetIBCKeeper().ConnectionKeeper.SetConnection(suite.chainB.GetContext(), proposedConnectionID, connectionEnd) | ||
upgrade.Fields.ConnectionHops[0] = proposedConnectionID | ||
}, | ||
connectiontypes.ErrInvalidConnectionState, | ||
}, | ||
{ | ||
"proposed connection ends are not each other's counterparty", | ||
func() { | ||
// reuse existing connection to create a new connection in a non OPEN state | ||
connectionEnd := path.EndpointB.GetConnection() | ||
// ensure counterparty connectionID does not match connectionID set in counterparty proposed upgrade | ||
connectionEnd.Counterparty.ConnectionId = "connection-50" | ||
|
||
// set proposed connection in state | ||
proposedConnectionID := "connection-100" | ||
suite.chainB.GetSimApp().GetIBCKeeper().ConnectionKeeper.SetConnection(suite.chainB.GetContext(), proposedConnectionID, connectionEnd) | ||
upgrade.Fields.ConnectionHops[0] = proposedConnectionID | ||
}, | ||
types.ErrIncompatibleCounterpartyUpgrade, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
tc := tc | ||
suite.Run(tc.name, func() { | ||
suite.SetupTest() | ||
|
||
path = ibctesting.NewPath(suite.chainA, suite.chainB) | ||
suite.coordinator.Setup(path) | ||
|
||
upgradeVersion := fmt.Sprintf("%s-v2", mock.Version) | ||
damiannolan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
path.EndpointA.ChannelConfig.ProposedUpgrade.Fields.Version = upgradeVersion | ||
err := path.EndpointA.ChanUpgradeInit() | ||
suite.Require().NoError(err) | ||
|
||
// ensure proof verification succeeds | ||
err = path.EndpointB.UpdateClient() | ||
suite.Require().NoError(err) | ||
|
||
// same construction used by endpoint.ChanUpgradeTry | ||
channelKey := host.ChannelKey(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) | ||
proofChannel, proofHeight := path.EndpointA.Chain.QueryProof(channelKey) | ||
upgradeKey := host.ChannelUpgradeKey(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) | ||
proofUpgrade, _ := path.EndpointA.Chain.QueryProof(upgradeKey) | ||
damiannolan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
counterpartyChannel = path.EndpointA.GetChannel() | ||
|
||
var found bool | ||
counterpartyUpgrade, found = path.EndpointA.Chain.App.GetIBCKeeper().ChannelKeeper.GetUpgrade(path.EndpointA.Chain.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) | ||
suite.Require().True(found) | ||
|
||
// ensure that the channel has a valid upgrade sequence | ||
channel := path.EndpointB.GetChannel() | ||
channel.UpgradeSequence = 1 | ||
path.EndpointB.SetChannel(channel) | ||
|
||
upgrade = types.Upgrade{ | ||
Fields: types.UpgradeFields{ | ||
Ordering: types.UNORDERED, | ||
ConnectionHops: []string{path.EndpointB.ConnectionID}, | ||
Version: upgradeVersion, | ||
}, | ||
Timeout: types.NewUpgradeTimeout(path.EndpointA.Chain.GetTimeoutHeight(), 0), | ||
LatestSequenceSend: 1, | ||
} | ||
|
||
tc.malleate() | ||
|
||
err = suite.chainB.GetSimApp().IBCKeeper.ChannelKeeper.StartFlushUpgradeHandshake( | ||
suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, upgrade.Fields, | ||
counterpartyChannel, counterpartyUpgrade, proofChannel, proofUpgrade, proofHeight, | ||
) | ||
|
||
if tc.expError != nil { | ||
suite.Require().Error(err) | ||
|
||
suite.Require().True(errorsmod.IsOf(err, tc.expError), err) | ||
} else { | ||
suite.Require().NoError(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func (suite *KeeperTestSuite) TestValidateProposedUpgradeFields() { | ||
var ( | ||
proposedUpgrade *types.UpgradeFields | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a strong reason that we don't just want to let
StartFlushUpgradeHandshake
be an exported function and not need these wrappers?To me, adding this additional level of indirection hurts my ability to reason about where things can be used in certain ways and why.
I don't think it's so bad to just export
k.startFlushUpgradeHandshake
.This is a mild preference though, if the general consensus is to have this additional wrapper I'm okay with it 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a quick check on this since it wasn't clear if having the function in a
_test.go
file would prevent it being used.I added the method sig to the expected keeper interface of
ChannelKeeper
in transfer and there was no compiler issues and you could call the method with no issues. So to me, it seems like just making it exported from the get go is the same thingThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait, the function should not be added to the binary in non-test mode, this is in
go help build
:the compiler as in the LSP that does checking in the IDE or does a
go build
succeed? (the second case would sound like a serious bug 😄, edit: well, so would the first, actually)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You sir are correct. It was the LSP that has me misled. A
make install
fails rightly as you say!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
startFlushUpgradeHandshake
should only be used during the upgrade handshake and thus does not need to be referenced outside of the04-channel/keeper
package. Given that our versioning is dependent upon API breaking changes, it gives us more flexibility to handle fixes without incrementing our major version if this function is unexported. In my opinion, only theMsg
's and grpc handlers should be exported. It is unsafe to reference this function outside of the context it is called in (the upgrade message handlers).I think in general we should aim to limit our public API as it gives us maximum development flexibility while also establishing a clear public API external developers can rely on. Unless there's a reason to make the function public, I think it is healthy to default to leaving the function private