Skip to content

Connection handshake

Carlos Rodriguez edited this page May 20, 2022 · 17 revisions

This document attempts to explore IBC's connection handshake in detail, showing as much practical information as possible.

Setup

  • Two chains (chain1 and chain2) running ib-go simd binary.
  • hermes relayer.

This document is updated for ibc-go v3.0.0 and hermes 0.14.0.

Assumptions

This tutorial assumes that both chains have already gone through the creation of a light client for the counterparty chain. In this tutorial the client ID of the light client on both chains is 07-tendermint-0.

ICS-03 store paths

Connection-related information is stored in the IBC store:

key value
[]byte("nextConnectionSequence") Next available connection sequence.
[]byte("connections/" + {connection-id}) Connection end
[]byte("clients/" + {clientID} + "/connections") Set of connection IDs associated with a particular client ID

Connection handshake

> ./hermes -c config.toml create connection chain1 chain2

Connection handshake in hermes.

ConnectionOpenInit

In the first step of the connection handshake the relayer submits MsgConnectionOpenInit on chain1 (see the message proto definition).

In hermes MsgConnectionOpenInit is constructed and sent in function build_conn_init_and_send. More specifically, the message is constructed in build_conn_init.

The parameters needed to construct MsgConnectionOpenInit are:

  • client_id is the client ID of chain1 (07-tendermint-0 in this tutorial).
  • counterparty is constructed here:
    • client_id is the client ID of chain2 (07-tendermint-0 in this tutorial).
    • connection_id is empty, since chain2 does not have a connection ID yet.
    • prefix is assigned at the moment the value read from the config file of hermes. In the future this value could be queried from chain2. This prefix is the store prefix used by the on-chain IBC module for Cosmos-SDK chains and at the moment is ibc.
  • version is assigned the default value. In the future this value could also be queried from chain2. This default value matches ibc-go's DefaultIBCVersion ("identifier": "1", "features": ["ORDER_ORDERED", "ORDER_UNORDERED"]).
  • delay_period is assigned the value entered in the command line parameter of the create connection command of hermes, if any; otherwise it takes the default value 0. The delay period is the time that must pass before a consensus state can be used for packet verification.
  • signer is the address of the relayer that submits the message.

From the hermes log we can retrieve the hash of the transaction that executed MsgConnectionOpenInit:

2022-04-23T19:52:36.928880Z DEBUG ThreadId(07) send_tx_commit{id=ConnectionOpenInit}:send_tx{id=chain1}: broadcast_tx_sync: Response { code: Ok, data: Data([]), log: Log("[]"), hash: transaction::Hash(E5078546601DC954E2B3461834B52AA4A2E6ED53EE947E8D6D4F29388120FFF7) }

And we can use this hash to get the transaction information using Tendermint's /tx RPC endpoint:

http://localhost:27000/tx?hash=0xE5078546601DC954E2B3461834B52AA4A2E6ED53EE947E8D6D4F29388120FFF7

See sample JSON result.

The value in the field result.tx is a base64-encoded string of the bytes of the messages that were executed as part of the transaction. This transaction contains only one message (MsgConnectionOpenInit) and if we decode these bytes we can retrieve back the message data that the relayer submitted:

client_id:"07-tendermint-0" 
counterparty:
<
  client_id:"07-tendermint-0" 
  prefix:
  <
    key_prefix:"ibc" 
  > 
> 
version:
<
  identifier:"1" 
  features:"ORDER_ORDERED" 
  features:"ORDER_UNORDERED" 
>
signer:"cosmos1cq6cexgmryrsjnghlu3fjjzfnyy07z8ap0cfhm"

The counterparty here is chain2.

This picture shows the execution flow of MsgConnectionOpenInit in ibc-go:

After the message reaches the message server the execution continues in ConnectionKeeper's ConnOpenInit function, to which the message fields are passed in. The picture above shows also the correspondance between the implementation and the spec for handling a connection open init request. The steps in ConnOpenInit consist of:

Step 1

types.GetCompatibleVersions returns the latest supported version of IBC used in connection version negotiation. These versions will be used in the creation of the connection end if the message does not provide any or the one provided is not supported. types.IsSupportedVersion returns true if the proposed version has a matching version in the list of compatible versions.

Step 2

A new connection identifier is generated by retrieving the next connection sequence from the store. In this tutorial, the generated connection ID is connection-0.

Step 3

A new ConnectionEnd is instantiated with the information from the message and with the state INIT.

The connection end is stored under the key connections/connection-0. The connection ID is also added to the set of connections associated with a client (which is stored under key clients/"07-tendermint-0/connections in this tutorial).

Step 4

An event is emitted signalling that the connection open init has finished successfully. From the event information (which can also be found in the field result.tx_result.log of the transaction that included MsgConnectionOpenInit) it is possible to figure out what connection ID this new connection end has:

{
  "events": [
    {
      "type": "connection_open_init",
      "attributes": [
        {
          "key": "connection_id",
          "value": "connection-0"
        },
        {
          "key": "client_id",
          "value": "07-tendermint-0"
        },
        {
          "key": "counterparty_client_id",
          "value": "07-tendermint-0"
        },
        {
          "key": "counterparty_connection_id"
        }
      ]
    },
    {
      "type": "message",
      "attributes": [
        {
          "key": "action",
          "value": "/ibc.core.connection.v1.MsgConnectionOpenInit"
        },
        {
          "key": "module",
          "value": "ibc_connection"
        }
      ]
    }
  ]
}

After MsgConnectionOpenInit successfully executes, there is a connection end stored on chain1. We can use ibc-go's REST interface to check the existence of the connection end with connection ID connection-0:

http://localhost:27001/ibc/core/connection/v1/connections/connection-0
{
  "connection": {
    "client_id": "07-tendermint-0",
    "versions": [
      {
        "identifier": "1",
        "features": [
          "ORDER_ORDERED",
          "ORDER_UNORDERED"
        ]
      }
    ],
    "state": "STATE_OPEN",
    "counterparty": {
      "client_id": "07-tendermint-0",
      "connection_id": "connection-0",
      "prefix": {
        "key_prefix": "aWJj"
      }
    },
    "delay_period": "0"
  },
  "proof": null,
  "proof_height": {
    "revision_number": "0",
    "revision_height": "10008"
  }
}

ConnectionOpenTry

After MsgConnectionOpenInit succeeds on chain1, then the relayer submits MsgConnectionOpenTry on chain2 (see the message proto definition.

In hermes MsgConnectionOpenTry is constructed and sent in function build_conn_try_and_send. More specifically, the message is constructed in build_conn_try.

To build MsgConnectionOpenInit hermes queries chain1 at a certain height to get proofs for the following things:

  • A proof that chain1 has stored a connection end with connection ID connection-0 and state INIT.
  • A proof that chain1 has stored for client ID 07-tendermint-0 the client state for chain2.
  • A proof that chain1 has stored for client ID 07-tendermint-0 the consensus state for chain2.

In this tutorial the height at which hermes is going to query chain1 for these proofs is 5. The query height is obtained here by querying Tendermint's /status RPC endpoint and reading the values for result.node_info.network (which is the revision number, chain1) and result.sync_info.latest_block_height (which is the revision height, 5).

The parameters needed to construct MsgConnectionOpenInit are thus:

  • client_id is the client ID of chain2 (07-tendermint-0 in this tutorial).
  • previous_connection_id is the connection ID of chain2 in case there exists a connection already in state INIT. The way hermes figures this out is by sending a query request to ibc-go's gRPC query endpoint for a single connection. This query returns the connection end on chain1, which includes the counterparty information with its respective connection_id. In this tutorial the previous_connection_id is empty, since there is no connection on chain2 in state INIT and therefore there is no connection end on chain2.
  • client_state is the client state for chain2 that chain1 has stored. It is retrieved here by performing an ABCI RPC query to the ibc store of chain1. We will see a sample of the client state data in the proof_client item a bit further down.
  • counterparty is constructed here:
    • client_id is the client ID of chain1 (07-tendermint-0 in this tutorial).
    • connection_id is the connection ID for the connection end on chain1, which is connection-0 in this tutorial.
    • prefix is determined here at the moment in a similar way as it was done for MsgConnectionOpenInit (i.e. reading the value ibc from the config file of hermes).
  • delay_period is determined here.
  • counterparty_versions is assigned here the versions from the connection end on chain1, if they exist; or the default, otherwise (as done similarly for MsgConnectionOpenInit).
  • proof_height is the height for the commitment root for proving the proofs for init, client and consensus. When creating these proofs, chain1 is queried at height proof_height - 1 (i.e. 5 in this tutorial).
  • proof_init is obtained by performing an ABCI RPC query to the ibc store of chain1. We can make the same ABCI RPC query on the browser by simply using Tendermint's /abci_query REST endpoint:
path: `store/ibc/key`
data: `connections/connection-0` (in hex: 0x636f6e6e656374696f6e732f636f6e6e656374696f6e2d30)
prove: true (so that the response contains the merkle proof)
height: 5
URL: http://localhost:27000/abci_query?path="store/ibc/key"&data=0x636f6e6e656374696f6e732f636f6e6e656374696f6e2d30&prove=true&height=5

See sample JSON output.

If we take base64-encoded byte string from the value for the field result.response.value and we decode it, then we can inspect the information for the connection end stored in chain1:

{
  ClientId:07-tendermint-0
  Versions:[
    identifier:"1" 
    features:"ORDER_ORDERED" 
    features:"ORDER_UNORDERED" 
  ]
  State:STATE_INIT 
  Counterparty:{
    ClientId:07-tendermint-0 
    ConnectionId: 
    Prefix: {
      KeyPrefix:[105 98 99]
    }
  } 
  DelayPeriod:0
}

The key_prefix is byte representation of the string "ibc".

Out of curiosity we can also take the value for the result.response.proofOps field and decode it to retrieve the merkle proof:

{
  [
    exist:
    <
      key:"connections/connection-0" value:"\n\01707-tendermint-0\022#\n\0011\022\rORDER_ORDERED\022\017ORDER_UNORDERED\030\001\"\030\n\01707-tendermint-0\032\005\n\003ibc" 
      leaf:
      <
        hash:SHA256 
        prehash_value:SHA256 
        length:VAR_PROTO 
        prefix:"\000\002\010" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\004\006\n {\351\212\t$\037\326\325\251\210\025/\271\243\335\200M\221\327\023\006\204Vo\217\242\237\270\220\303R\n " 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\006\014\n " 
        suffix:" \"\014\317[\032e\236\\Vj\252\227\036rO\371\247@\013\267\022\234\001\372m\245\027\377\3717~K" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\n\034\n \026\t\324\340\335\2416\232\370f_\261{\321\375\326\345\n\025\365\214\303r\250\374\213\341\002\243\2402t " 
      > 
    >  
    exist:
    <
      key:"ibc" 
      value:"\277\364\362YM\200\266\241\232}\377F\220g9cqxc\232N\313L)\307\231\345)]=\225\364" 
      leaf:
      <
        hash:SHA256 
        prehash_value:SHA256 
        length:VAR_PROTO 
        prefix:"\000" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\001" 
        suffix:"\305\005\300\375H\261\317+ea\237\022\263\024N\031\306\013NkbR^\3516\276\223\320Ab>\277" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\001" 
        suffix:"@\251\313\2625\350\307\246\356 \356\305\247\231 @E\275\016\3256<6\270\036X;\241\023\230_\372" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\001" 
        suffix:"\323C\255q0\267:\211\365Q\203\221\013\260k\237\255\245\206<\235M2y\254\236\233\352\373S\241:" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\001\304\300j8q~\n{V\031\252\277\"{l=V\344\200\365\377F|D\\\260D\207\023\023J\202" 
      > 
      path:
      <
        hash:SHA256 
        prefix:"\001" 
        suffix:"\367\235\232\0273q2\204\220#\225k\302\334\350\370\262\206\366@4\024\004_\037\243\037M\332K\272i" 
      > 
    > 
  ]
}
  • proof_client is obtained by performing an ABCI RPC query to the ibc store of chain1. This query intends to check that chain1 has stored for client ID 07-tendermint-0 the client state for chain2 at a certain height (5 in this tutorial). We can make the same ABCI RPC query on the browser by simply using Tendermint's /abci_query REST endpoint:
path: store/ibc/key
data: clients/07-tendermint-0/clientState (in hex: 0x636c69656e74732f30372d74656e6465726d696e742d302f636c69656e745374617465)
prove: true (so that the response contains the merkle proof)
height: 5
http://localhost:27000/abci_query?path="store/ibc/key"&data=0x636c69656e74732f30372d74656e6465726d696e742d302f636c69656e745374617465&prove=true&height=5

See sample JSON output.

If we take base64-encoded byte string from the value for the field result.response.value and we decode it, then we can inspect the information for the client state of chain2 stored in chain1:

chain_id:"chain2"
trust_level:
<
  numerator:1 
  denominator:3
> 
trusting_period:
<
  seconds:1209600
> 
unbonding_period:
<
  seconds:1814400
> 
max_clock_drift:
<
  seconds:20
> 
frozen_height:
<
> 
latest_height:
<
  revision_height:3
> 
proof_specs:
<
  leaf_spec:
  <
    hash:SHA256 
    prehash_value:SHA256 
    length:VAR_PROTO 
    prefix:"\000" 
  >
  inner_spec:
  <
    child_order:0
    child_order:1
    child_size:33
    min_prefix_length:4
    max_prefix_length:12
    hash:SHA256 
  > 
> 
proof_specs:
<
  leaf_spec:
  <
    hash:SHA256
    prehash_value:SHA256
    length:VAR_PROTO
    prefix:"\000"
  >
  inner_spec:
  <
    child_order:0
    child_order:1
    child_size:32
    min_prefix_length:1
    max_prefix_length:1
    hash:SHA256 
  >
> 
upgrade_path:"upgrade" 
upgrade_path:"upgradedIBCState" 
allow_update_after_expiry:true 
allow_update_after_misbehaviour:true 

We see that the latest_height has a revision_height of 3, which means that the client state for chain2 was lastt updated for block height 3 of chain2.

  • proof_consensus is obtained by performing an ABCI RPC query to the ibc store of chain1. This query intends to check that chain1 has stored at a certain height (5 in this tutorial) for client ID 07-tendermint-0 the consensus state at a given height (3 in this tutorial) for chain2. We can make the same ABCI RPC query on the browser by simply using Tendermint's /abci_query REST endpoint. The data query parameter follows the format clients/{client-id}/consensusStates/{epoch}-{height}. epoch is the revision number (0 in this tutorial) and height is the revision height (3 in this tutorial), both retrieved from the latest_height field of the client state stored on chain1.
path: store/ibc/key
data: clients/07-tendermint-0/consensusStates/0-3 (in hex 0x636c69656e74732f30372d74656e6465726d696e742d302f636f6e73656e7375735374617465732f302d33)
prove: true
height: 5
http://localhost:27000/abci_query?path="store/ibc/key"&data=0x636c69656e74732f30372d74656e6465726d696e742d302f636f6e73656e7375735374617465732f302d33&prove=true&height=5

See sample JSON output.

If we take base64-encoded byte string from the value for the field result.response.value and we decode it, then we can inspect the information for the consensus state of chain2 (at block height 3) stored in chain1 (at block height 5):

timestamp:
<
  seconds:1650743551 
  nanos:180695000 
> 
root:
<
  hash:"4\261\263\352|=\367D\001\325\001\310o\347n\242\220\226z\377Ds\366\247\006cM\371\326\3379\206" 
> 
next_validators_hash:"d\323Z2\216/\0039\002\351\311\351m\017.\002o\342g\311s\223\rm\202\336\026\006\242\032\354\240" 

The timestamp corresponds to the block height in which the consensus state was stored. root is the app hash for block 3 of chain2 and next_validators_hash is the hash of the next validator set. If we query chain2 locally for block at height 3 by going on the browser to http://localhost:27010/block?height=3 we can check that the value in the response for the field result.block.header.app_hash matches the root hash from the consensus state (see the sample JSON output).

  • consensus_height is the latest height of chain2 which chain1 has stored in its chain2 light client. In this tutorial it is 3 (i.e. the value for latest_height in the stored client state).
  • signer is the address of the relayer that submits the message.

MsgConnectionOpenTry

{ClientId:07-tendermint-0 Versions:[identifier:"1" features:"ORDER_ORDERED" features:"ORDER_UNORDERED" ] State:STATE_INIT Counterparty:{ClientId:07-tendermint-0 ConnectionId: Prefix:{KeyPrefix:[105 98 99]}} DelayPeriod:0}
Clone this wiki locally