With the inception of blockchains in 2019, more and more came around with different innovations, some targeting the shortcomings, some targeting to create something new, while others just came and went away. As many as 126 Blockchains are live now, like Ethereum, Bitcoin, Cosmos, and Polkadot, to name a few. This did bring new ideas to the paradigm, but DApps built on top of these blockchains face liquidity fragmentation and composability issues, to name a few problems. This may be solved by enabling interoperability between these blockchains. While many bridges are in the picture targeting to provide interoperability, the security of those isn't very ideal as of today. This brings us to IBC, a protocol that allows blockchains to communicate but unlike most bridges, it doesnt have committees. In this article, we will have a look at how IBC is used to transfer data packages, enabling blockchains to communicate, providing interoperability. IBC was launched as part of the Cosmos ecosystem; although it is not just for Cosmos, any blockchain can make use of it. Without further ado, let's dive in. Read about the Cosmos Architecture. Inter-blockchain communication, or IBC, allows blockchains to talk with each other. It provides a standard for data structures, semantics, protocol, and framework that enable every blockchain implementing IBC to authenticate and transport data among them in a permissionless manner, providing interoperability. Chains built with Cosmos SDK can implement IBC quite easily as it is part of the SDK. But that doesn’t mean chains with consensus other than Tendermint can’t adopt IBC. They can, although the implementation will need modifications. To implement IBC, we need to implement a minimal set of functions as per ICS standards. IBC comprises of a transport layer, an application layer, light clients, and relayers. Let’s go through the basic functionality of each of these. As we already know transport layer is responsible for transferring, authenticating, and ordering of messages. To do that, we first need a connection between the concerned blockchains. The connection is built via a 4-way handshake. Let’s explore how that is accomplished while observing a reference implementation. For the sake of simplicity, we will consider chain A and chain B, which are trying to build a connection. The first thing to establish a connection is to initialize the connection. The function first checks for version compatibility. Then we set the values of the function NewConnectionEnd, which include The function creates a unique connection ID and adds it to the list of connections associated with the chain. In this function, clientID is chain A, and the counterparty is chain B. This function tells chain B that chain A is trying to open a connection between them. Again, NewConnectionEnd is updated. This function creates a new counterparty instance and establishes a new connection on this end. Chain B chooses the available IBC-compatible version with chain A. In a nutshell, chain B verifies the ID of chain A, tries to open the connection by verifying that the chains are indeed compatible, and counter-checks whether chain A has the correct information. To make sure that the state update was successful, the relayer relays this update to the light clients. Through this function, chain B acknowledges the connection request initiated by chain A by retrieving the connection, checking the state of the connections, checking the client’s states, and then updating the connection state to OPEN. This function confirms that the connection is established between chain A and chain B. It first retrieves the connections and checks that the state is TRYOPEN on chain B and OPEN on chain a. Then it sets the state of connection on chain B to OPEN, hence successfully opening the connection. Needless to say, in case of being unable to fulfill any check, the connection doesn’t go through. Once the connection is built, we need channels to transfer the message packages along the channels. Do note that, connections can have multiple channels. To initiate a channel, again, a 4-way communication is initiated. This function is called by modules to initiate a channel on the source chain. The counterparty's current channelID sequence is validated to be empty. Chain A opens channel with it hence setting the channelID. It gets the connection information, and does necessary checks like whether the version is agreed upon and supported. The caller of the function owns the portID to initiate a channel, then finally generates the channel identifier. Channel type for chain A is set to INIT. An application can only do that if it has the command of the channel and port, this function runs the checks to ensure that as well. The counterparty accepts the open channel request through this function. Generates the channel identifier for chain B, and does the same necessary checks which ChanOpenInit function did, only in that function, it was for the source chain, and in this function it is for the counterparty chain. Then, sets the expected ID for counterparty i.e., chain B, and the expected channel. These are later used by chain A for acknowledgment. Channel type for chain B is set to INIT. Source chain runs this function to acknowledge the acceptance of counterparty chain. This makes the channel live by setting the channel type to OPENTRY. Counterparty confirms the channel opening with the source chain by calling this function. It sets the channel type to OPEN, which is the confirmation indication. This successfully opens the channel between the two chains. Again, we didn't talk about error conditions and failure redirection, but in that case, the algorithm wouldn’t enable the channel. With the completion of these 2 four-way-handshakes, we shall have live connections and channels. Why did we create these? To transfer packages along the path, so our next step is exactly that. Three functions make sure the sending of message packages. Let’s have a look at them in this section. The transferring is done from the application layer, and the relayer is responsible to do that. This function is called to send a message package to the channel. It first calls the GetChannel function, which returns the identifier bound with the port. Then checks that the state of the channel is not closed and the caller chain, Chain A, has the capability of the port. Then, it creates the package instance, which takes in the parameters Data i.e., custom data for a package, Sequence i.e., the sequence number of packets in the channel, SourcePort, SourceChannel, DestinationPort, DestinationChannel, TimeoutHeight, TimeoutTimestamp. The basic validation for channel fields is also done in this function. Then, it checks that the package is not timed out and finally commits the package. The counterparty calls this function to receive and process the IBC message package. It runs the same checks to verify that the counterparty has the command of the channel and the port it is trying to authenticate receiving the packet from. Checks that the connection is OPEN, and that the package is not timed out. Depending upon whether the channel ordering parameter is ordered or unordered, the function checks / arranges the sequence. The source chain receives the acknowledgment of the counter-party chain. This function first does the necessary checks, which are the same as the previous two functions, then verifies the package acknowledgment. Then since the package commitment is of no use anymore, it deletes it, finally logs the acknowledgment, and with that, we are done. Figure 2 visually represents the functions mentioned in the section “Sending Message packages.” Blockchain bridges depend on committees for security. What does IBC make use of for security reasons? IBC Clients. So, what are IBC clients? They are onchain light clients that track the consensus state of other blockchains. They also track proof specifications to verify the proof against the client consensus state. IBC light clients also track and verify the state of other blockchains; it does so for commitment proof of sending and receiving messages on the source and destination blockchains. The benefit of that is you don’t trust relayers or any third party. You trust these proofs provided by Light clients. In the case where all the relayers are malicious and send the wrong packages, the packages will get declined because the proof generated won’t be valid. IBC already has client implementations such as Solo Machine Light Client, Tendermint light client, and Localhost (loopback) client. We previously established that messages and proof are sent to the destination chain; to do that, we open channels and create connections. The transferring of these packages is done by relayer. Relayers ensure that the packages are relayed for that they have access to full nodes. Our package sending and receiving functions emit events at the completion. Relayers listen for these events. You can create a relayer software or use an existing one. A relayer software must have information about the chains between which it is relaying the messaging. We have Go-Relayer and Hermez-Relayer clients available. IBC opens up the connection to send and receive packages, whether the chain is malicious or not; that is something IBC has no concern with. We’ve already looked at how light clients are responsible for wrong message packets. The application layer in IBC works similarly to the transport layer, except it has additional information i.e, encoding, decoding, and interpretation of the data to trigger the custom application logic. Inter-Blockchain Communication (IBC) is a protocol that allows blockchains to communicate with each other and exchange data. It provides a standard for data structures, semantics, protocol, and framework that enables every blockchain implementing IBC to authenticate and transport data among them in a permissionless manner, providing interoperability. IBC consists of a transport layer, an application layer, light clients, and relayers. The transport layer is responsible for transferring, authenticating, and ordering messages between the blockchains. The application layer takes care of how these messages should be packaged and interpreted. The light client of the remote state machine is an algorithm that enables actors to verify state updates of that state machine without having to execute the state machine. Finally, the relayer helps establish connections, create clients, channels, and paths between chains, and relays messages along those paths. IBC can be implemented by any blockchain, although the implementation may require modifications for blockchains with consensus mechanisms other than Tendermint.Inter-Blockchain Communication
Transport Layer
Build Connections
func (k Keeper) ConnOpenInit(
ctx sdk.Context,
clientID string,
counterparty types.Counterparty, // counterpartyPrefix, counterpartyClientIdentifier
version *types.Version,
delayPeriod uint64,
) (string, error) {
versions := types.GetCompatibleVersions()
if version != nil {
if !types.IsSupportedVersion(types.GetCompatibleVersions(), version) {
return "", sdkerrors.Wrap(types.ErrInvalidVersion, "version is not supported")
}
versions = []exported.Version{version}
}
connectionID := k.GenerateConnectionIdentifier(ctx)
if err := k.addConnectionToClient(ctx, clientID, connectionID); err != nil {
return "", err
}
// connection defines chain A's ConnectionEnd
connection := types.NewConnectionEnd(types.INIT, clientID, counterparty, types.ExportedVersionsToProto(versions), delayPeriod)
k.SetConnection(ctx, connectionID, connection)
k.Logger(ctx).Info("connection state updated", "connection-id", connectionID, "previous-state", "NONE", "new-state", "INIT")
defer func() {
telemetry.IncrCounter(1, "ibc", "connection", "open-init")
}()
EmitConnectionOpenInitEvent(ctx, connectionID, clientID, counterparty)
return connectionID, nil
}
func (k Keeper) ConnOpenTry(
ctx sdk.Context,
counterparty types.Counterparty, // counterpartyConnectionIdentifier, counterpartyPrefix and counterpartyClientIdentifier
delayPeriod uint64,
clientID string, // clientID of chainA
clientState exported.ClientState, // clientState that chainA has for chainB
counterpartyVersions []exported.Version, // supported versions of chain A
proofInit []byte, // proof that chainA stored connectionEnd in state (on ConnOpenInit)
proofClient []byte, // proof that chainA stored a light client of chainB
proofConsensus []byte, // proof that chainA stored chainB's consensus state at consensus height
proofHeight exported.Height, // height at which relayer constructs proof of A storing connectionEnd in state
consensusHeight exported.Height, // latest height of chain B which chain A has stored in its chain B client
) (string, error) {
// generate a new connection
connectionID := k.GenerateConnectionIdentifier(ctx)
selfHeight := clienttypes.GetSelfHeight(ctx)
if consensusHeight.GTE(selfHeight) {
return "", sdkerrors.Wrapf(
sdkerrors.ErrInvalidHeight,
"consensus height is greater than or equal to the current block height (%s >= %s)", consensusHeight, selfHeight,
)
}
// validate client parameters of a chainB client stored on chainA
if err := k.clientKeeper.ValidateSelfClient(ctx, clientState); err != nil {
return "", err
}
expectedConsensusState, err := k.clientKeeper.GetSelfConsensusState(ctx, consensusHeight)
if err != nil {
return "", sdkerrors.Wrapf(err, "self consensus state not found for height %s", consensusHeight.String())
}
// expectedConnection defines Chain A's ConnectionEnd
// NOTE: chain A's counterparty is chain B (i.e where this code is executed)
// NOTE: chainA and chainB must have the same delay period
prefix := k.GetCommitmentPrefix()
expectedCounterparty := types.NewCounterparty(clientID, "", commitmenttypes.NewMerklePrefix(prefix.Bytes()))
expectedConnection := types.NewConnectionEnd(types.INIT, counterparty.ClientId, expectedCounterparty, types.ExportedVersionsToProto(counterpartyVersions), delayPeriod)
// chain B picks a version from Chain A's available versions that is compatible
// with Chain B's supported IBC versions. PickVersion will select the intersection
// of the supported versions and the counterparty versions.
version, err := types.PickVersion(types.GetCompatibleVersions(), counterpartyVersions)
if err != nil {
return "", err
}
// connection defines chain B's ConnectionEnd
connection := types.NewConnectionEnd(types.TRYOPEN, clientID, counterparty, []*types.Version{version}, delayPeriod)
// Check that ChainA committed expectedConnectionEnd to its state
if err := k.VerifyConnectionState(
ctx, connection, proofHeight, proofInit, counterparty.ConnectionId,
expectedConnection,
); err != nil {
return "", err
}
// Check that ChainA stored the clientState provided in the msg
if err := k.VerifyClientState(ctx, connection, proofHeight, proofClient, clientState); err != nil {
return "", err
}
// Check that ChainA stored the correct ConsensusState of chainB at the given consensusHeight
if err := k.VerifyClientConsensusState(
ctx, connection, proofHeight, consensusHeight, proofConsensus, expectedConsensusState,
); err != nil {
return "", err
}
// store connection in chainB state
if err := k.addConnectionToClient(ctx, clientID, connectionID); err != nil {
return "", sdkerrors.Wrapf(err, "failed to add connection with ID %s to client with ID %s", connectionID, clientID)
}
k.SetConnection(ctx, connectionID, connection)
k.Logger(ctx).Info("connection state updated", "connection-id", connectionID, "previous-state", "NONE", "new-state", "TRYOPEN")
defer func() {
telemetry.IncrCounter(1, "ibc", "connection", "open-try")
}()
EmitConnectionOpenTryEvent(ctx, connectionID, clientID, counterparty)
return connectionID, nil
}
func (k Keeper) ConnOpenAck(
ctx sdk.Context,
connectionID string,
clientState exported.ClientState, // client state for chainA on chainB
version *types.Version, // version that ChainB chose in ConnOpenTry
counterpartyConnectionID string,
proofTry []byte, // proof that connectionEnd was added to ChainB state in ConnOpenTry
proofClient []byte, // proof of client state on chainB for chainA
proofConsensus []byte, // proof that chainB has stored ConsensusState of chainA on its client
proofHeight exported.Height, // height that relayer constructed proofTry
consensusHeight exported.Height, // latest height of chainA that chainB has stored on its chainA client
) error {
// Check that chainB client hasn't stored invalid height
selfHeight := clienttypes.GetSelfHeight(ctx)
if consensusHeight.GTE(selfHeight) {
return sdkerrors.Wrapf(
sdkerrors.ErrInvalidHeight,
"consensus height is greater than or equal to the current block height (%s >= %s)", consensusHeight, selfHeight,
)
}
// Retrieve connection
connection, found := k.GetConnection(ctx, connectionID)
if !found {
return sdkerrors.Wrap(types.ErrConnectionNotFound, connectionID)
}
// verify the previously set connection state
if connection.State != types.INIT {
return sdkerrors.Wrapf(
types.ErrInvalidConnectionState,
"connection state is not INIT (got %s)", connection.State.String(),
)
}
// ensure selected version is supported
if !types.IsSupportedVersion(types.ProtoVersionsToExported(connection.Versions), version) {
return sdkerrors.Wrapf(
types.ErrInvalidConnectionState,
"the counterparty selected version %s is not supported by versions selected on INIT", version,
)
}
// validate client parameters of a chainA client stored on chainB
if err := k.clientKeeper.ValidateSelfClient(ctx, clientState); err != nil {
return err
}
// Retrieve chainA's consensus state at consensusheight
expectedConsensusState, err := k.clientKeeper.GetSelfConsensusState(ctx, consensusHeight)
if err != nil {
return sdkerrors.Wrapf(err, "self consensus state not found for height %s", consensusHeight.String())
}
prefix := k.GetCommitmentPrefix()
expectedCounterparty := types.NewCounterparty(connection.ClientId, connectionID, commitmenttypes.NewMerklePrefix(prefix.Bytes()))
expectedConnection := types.NewConnectionEnd(types.TRYOPEN, connection.Counterparty.ClientId, expectedCounterparty, []*types.Version{version}, connection.DelayPeriod)
// Ensure that ChainB stored expected connectionEnd in its state during ConnOpenTry
if err := k.VerifyConnectionState(
ctx, connection, proofHeight, proofTry, counterpartyConnectionID,
expectedConnection,
); err != nil {
return err
}
// Check that ChainB stored the clientState provided in the msg
if err := k.VerifyClientState(ctx, connection, proofHeight, proofClient, clientState); err != nil {
return err
}
// Ensure that ChainB has stored the correct ConsensusState for chainA at the consensusHeight
if err := k.VerifyClientConsensusState(
ctx, connection, proofHeight, consensusHeight, proofConsensus, expectedConsensusState,
); err != nil {
return err
}
k.Logger(ctx).Info("connection state updated", "connection-id", connectionID, "previous-state", "INIT", "new-state", "OPEN")
defer func() {
telemetry.IncrCounter(1, "ibc", "connection", "open-ack")
}()
// Update connection state to Open
connection.State = types.OPEN
connection.Versions = []*types.Version{version}
connection.Counterparty.ConnectionId = counterpartyConnectionID
k.SetConnection(ctx, connectionID, connection)
EmitConnectionOpenAckEvent(ctx, connectionID, connection)
return nil
}
func (k Keeper) ConnOpenConfirm(
ctx sdk.Context,
connectionID string,
proofAck []byte, // proof that connection opened on ChainA during ConnOpenAck
proofHeight exported.Height, // height that relayer constructed proofAck
) error {
// Retrieve connection
connection, found := k.GetConnection(ctx, connectionID)
if !found {
return sdkerrors.Wrap(types.ErrConnectionNotFound, connectionID)
}
// Check that connection state on ChainB is on state: TRYOPEN
if connection.State != types.TRYOPEN {
return sdkerrors.Wrapf(
types.ErrInvalidConnectionState,
"connection state is not TRYOPEN (got %s)", connection.State.String(),
)
}
prefix := k.GetCommitmentPrefix()
expectedCounterparty := types.NewCounterparty(connection.ClientId, connectionID, commitmenttypes.NewMerklePrefix(prefix.Bytes()))
expectedConnection := types.NewConnectionEnd(types.OPEN, connection.Counterparty.ClientId, expectedCounterparty, connection.Versions, connection.DelayPeriod)
// Check that connection on ChainA is open
if err := k.VerifyConnectionState(
ctx, connection, proofHeight, proofAck, connection.Counterparty.ConnectionId,
expectedConnection,
); err != nil {
return err
}
// Update ChainB's connection to Open
connection.State = types.OPEN
k.SetConnection(ctx, connectionID, connection)
k.Logger(ctx).Info("connection state updated", "connection-id", connectionID, "previous-state", "TRYOPEN", "new-state", "OPEN")
defer func() {
telemetry.IncrCounter(1, "ibc", "connection", "open-confirm")
}()
EmitConnectionOpenConfirmEvent(ctx, connectionID, connection)
return nil
}
Build Channels
Send Message Packages
Clients:
Relayer
Application Layer