-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
ConnOpenTry() fails if a connection exists with uninitialized counterparty connection id #7870
Comments
cc @cwgoes |
What is an "uninitialized counterparty connection id"? How would a connection ever be created with one? Do you mean empty string? |
yes |
Right, OK, this needs a spec change. |
Is this just a spec change or a bug? I think I came across a similar issue during Informal Systems audit of crossing hellos just now, and it seems it's not just about the ConnectionID, it expects other parameters of the existing connection to match as well, eg. the clientID. So if ChainB has two clients for ChainA, call them client1 and client2, and then a crossing hello's happens with ConnOpenInit being called on both chains, but on ChainA it's called with reference to client1 (ie. counterparty.ClientID = client1) and on ChainB it's called with reference to client2 (ie. clientID = client2), but they use the correct/expected connection ID, then I think the connection will fail to ever open because this cosmos-sdk/x/ibc/core/03-connection/keeper/handshake.go Lines 116 to 122 in fe8a891
I didn't see a test for crossing connection hellos - are there any? I tried to write one for this scenario, but not sure I got it all right. This does seem to test crossing hellos and passes/fails in the expected spot : func (suite *KeeperTestSuite) TestConnOpen2Try() {
var (
clientA string
clientB, clientB2 string
versions []exported.Version
consensusHeight exported.Height
counterpartyClient exported.ClientState
)
f := func() {
// create clients on both chain
clientA, clientB = suite.coordinator.SetupClients(suite.chainA, suite.chainB, ibctesting.Tendermint)
// create another client for chain A on chainB
var err error
clientB2, err = suite.coordinator.CreateClient(suite.chainB, suite.chainA, ibctesting.Tendermint)
suite.Require().NoError(err)
// call OpenInit on chain A with reference to clientB
connA, connB, err := suite.coordinator.ConnOpenInit(suite.chainA, suite.chainB, clientA, clientB)
suite.Require().NoError(err)
fmt.Println("connA", connA)
fmt.Println("connB", connB)
// call OpenInit (crossing hello) on chain B with reference to clientB2 but with desired connectionID.
// this will cause the test to fail.
// Note if we call ConnOpenInit here with clientB instead of clientB2, the test passes
counterparty := types.NewCounterparty(clientA, connA.ID, suite.chainA.GetPrefix())
err = suite.chainB.App.IBCKeeper.ConnectionKeeper.ConnOpenInit(suite.chainB.GetContext(), connB.ID, clientB2, counterparty, nil)
suite.Require().NoError(err)
err = suite.coordinator.UpdateClient(suite.chainA, suite.chainB, clientA, ibctesting.Tendermint)
suite.Require().NoError(err)
err = suite.coordinator.UpdateClient(suite.chainB, suite.chainA, clientB, ibctesting.Tendermint)
suite.Require().NoError(err)
// retrieve client state of chainA to pass as counterpartyClient
counterpartyClient = suite.chainA.GetClientState(clientA)
}
suite.Run("crossing", func() {
suite.SetupTest() // reset
consensusHeight = clienttypes.ZeroHeight() // must be explicitly changed in malleate
versions = types.GetCompatibleVersions() // must be explicitly changed in malleate
f()
connA := suite.chainA.GetFirstTestConnection(clientA, clientB)
connB := suite.chainB.GetFirstTestConnection(clientB, clientA)
counterparty := types.NewCounterparty(clientA, connA.ID, suite.chainA.GetPrefix())
fmt.Println("connA, connB", connA, connB)
// get counterpartyChosenConnectionID
var counterpartyChosenConnectionID string
connection, found := suite.chainA.App.IBCKeeper.ConnectionKeeper.GetConnection(suite.chainA.GetContext(), connA.ID)
if found {
counterpartyChosenConnectionID = connection.Counterparty.ConnectionId
}
connectionKey := host.ConnectionKey(connA.ID)
proofInit, proofHeight := suite.chainA.QueryProof(connectionKey)
if consensusHeight.IsZero() {
// retrieve consensus state height to provide proof for
consensusHeight = counterpartyClient.GetLatestHeight()
}
consensusKey := host.FullConsensusStateKey(clientA, consensusHeight)
proofConsensus, _ := suite.chainA.QueryProof(consensusKey)
// retrieve proof of counterparty clientstate on chainA
clientKey := host.FullClientStateKey(clientA)
proofClient, _ := suite.chainA.QueryProof(clientKey)
fmt.Println("OPEN TRY")
err := suite.chainB.App.IBCKeeper.ConnectionKeeper.ConnOpenTry(
suite.chainB.GetContext(), connB.ID, counterpartyChosenConnectionID, counterparty, clientB, counterpartyClient,
versions, proofInit, proofClient, proofConsensus,
proofHeight, consensusHeight,
)
suite.Require().NoError(err)
})
}
|
There are a couple doing basic unit testing in connection/channel handshake but definitely not enough. We have an issue tracking it #7603. The test looks correct to me. On chainA it creates connection from clientA to clientB and on chainB it creates a connection from clientB2 to clientA, all with the same connectionID. Since the clients are different but the same connection IDs are used, with the current construction it is expected to fail |
I believe it's both. With empty counterparty connection id for existing connection we expect the check to pass but it fails. Probably this check (both in code and spec) was just overlooked when flexible connection id was introduced.
All the other parameters must be specified (i.e. non-empty strings) in the existing connection. For these the check fails, as expected, if they don't match the ones in |
But can't a malicious relayer race to do this every time it sees a ConnOpenInit and thus attempt to block anyone from ever opening a connection? |
Yes but I opened this issue just to fix quickly the case where we only deal with correct relayers. re: malicious relayer -> I think there should be another issue where we discuss this since it will probably be a longer discussion. Talking a few days ago with @milosevic and Ilina about this: I think the “correct” relayer that would guarantee that the handshake finishes would have to do something like:
So maybe the solution is to not allow the |
No, unless the initiator of the handshake specified a particular counterparty connection ID, in which case the relayer can race anyways. For the original issue outlined above I think we just need to check that either the counterparty connection ID matches or that our previously stored counterparty connection ID was unspecified (empty string) indicating that the counterparty should be allowed to pick it. |
@ancazamfir I've made the spec changes in cosmos/ibc#493 if you want to take a look. |
Summary of Bug
If a previous connection exists with uninitialized counterparty connection id,
ConnOpenTry
fails here:cosmos-sdk/x/ibc/core/03-connection/keeper/handshake.go
Line 118 in fe8a891
Haven't tested the channel case but looks like the same check is there:
cosmos-sdk/x/ibc/core/04-channel/keeper/handshake.go
Line 121 in fe8a891
Version
v0.40.0-rc0
but looks the same on masterSteps to Reproduce
Testing with raw commands from the Rust relayer from a development branch. The only difference with the Go relayer being that we allow building
OpenInit
with unspecified counterparty connection id.For Admin Use
The text was updated successfully, but these errors were encountered: