Channel lifecycle
A channel is a connection between two IBC ports that allows them to send packets to each other. In this section, we will cover how to establish a channel and how to close it. Since a channel is a connection between two ports, it can connect two chain modules, two contracts, or a module and a contract.
For the sake of readability, we will assume that both ends of the channel are contracts in the following explanation. For more general information about the channel lifecycle, see the ICS 004 specification (opens in a new tab) or IBC channel docs (opens in a new tab).
Each channel also has an order that can be either Ordered
or Unordered
. This is encoded in the
IbcOrder
(opens in a new tab) enum. In an ordered channel, packets must be processed by the receiving chain in the
order in which they were sent. In an unordered channel, packets are processed in the order they
arrive.
Establishing a channel
To send packets between two chains, you need to establish a channel between them. This process involves two calls per chain. Once the channel is established, you can start sending packets through it, which we will cover in the next section. To initiate the channel creation, you can use a relayer binary such as hermes (opens in a new tab) to perform the handshake between the two endpoints. Returning an error from one of the calls will cause the channel handshake to fail.
In the following, we will refer to the two chains we want to connect as chain A and B. The handshake starts on chain A.
We will take a closer look at the handshake process, but here is a brief summary of the steps:
ibc_channel_open
on chain A withIbcChannelOpenMsg::OpenInit
ibc_channel_open
on chain B withIbcChannelOpenMsg::OpenTry
ibc_channel_connect
on chain A withIbcChannelConnectMsg::OpenAck
ibc_channel_connect
on chain B withIbcChannelConnectMsg::OpenConfirm
Channel open
When the channel creation is started by the relayer, the first call to the contract is made on chain
A. This call is made to the ibc_channel_open
entrypoint with the IbcChannelOpenMsg::OpenInit
variant. Then the same entrypoint is called on chain B with the IbcChannelOpenMsg::OpenTry
variant. See the following example and the IbcChannelOpenMsg
(opens in a new tab) documentation.
use cw_storage_plus::Item;
/// enforces ordering and versioning constraints
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
deps: DepsMut,
env: Env,
msg: IbcChannelOpenMsg
) -> StdResult<IbcChannelOpenResponse> {
let channel = msg.channel();
// in this example, we only allow a single channel per contract instance
// you can do more complex checks here
ensure!(!CHANNEL.exists(deps.storage), StdError::generic_err("channel already exists"));
// we should check if the channel is what we expect (e.g. the order)
if channel.order != IbcOrder::Ordered {
return Err(StdError::generic_err("only ordered channels are supported"));
}
// the OpenTry variant (on chain B) also has the counterparty version
// we should check if it is what we expect
if let Some(counter_version) = msg.counterparty_version() {
if counter_version != IBC_APP_VERSION {
return Err(StdError::generic_err(format!(
"Counterparty version must be `{IBC_APP_VERSION}`"
)));
}
}
// now, we save the channel ID to storage, so we can use it later
// this also prevents any further channel openings
CHANNEL.save(deps.storage, &ChannelInfo {
channel_id: channel.endpoint.channel_id.clone(),
finalized: false,
})?;
// return the channel version we support
Ok(Some(Ibc3ChannelOpenResponse {
version: IBC_APP_VERSION.to_string(),
}))
}
#[cw_serde]
struct ChannelInfo {
channel_id: String,
/// whether the channel is completely set up
finalized: bool,
}
const CHANNEL: Item<ChannelInfo> = Item::new("channel");
const IBC_APP_VERSION: &str = "my-protocol-v1";
In the example above, we return the same version we expect from the counterparty, but you can return
a different version if the counterparty accepts it. The version is used to ensure that both chains
are running the protocol that the other one expects. You can also return None
if you just want to
accept the counterparty version.
Permissions
Opening a channel is generally a permissionless process, so make sure to keep that in mind when
implementing the entrypoints. In the examples above, we only allow a single channel per contract
instance and always make sure not to overwrite an existing channel. Note that we already save the
channel in ibc_channel_open
. This causes overlapping channel openings to fail the channel
handshake. The drawback is that the contract can only connect to that one channel, so if the
handshake fails, the contract cannot connect to another channel.
You can add additional checks to ensure that the channel is connecting to the correct counterparty or use a map to keep track of multiple channels connecting to different counterparties.
You can also be more restrictive and only allow the contract itself to initiate the channel creation
handshake. This can be done by adding a state item to the contract that is only set to true before
the contract sends the message to initiate the handshake and immediately set to false in the
ibc_channel_open
entrypoint.
Advanced example: Contract initiating the handshake on its own
The following example requires the cosmos-sdk-proto
crate that provides protobuf types for
Cosmos SDK messages.
use cosmos_sdk_proto::ibc::core::channel::v1::{
Channel, Counterparty, MsgChannelOpenInit, Order, State,
};
use cosmos_sdk_proto::traits::Message;
use cw_storage_plus::Item;
/// Creates a new Cosmos SDK message that initiates the channel handshake
pub fn new_msg_channel_open_init(
deps: Deps,
env: &Env,
connection_id: String,
counterparty_port_id: String,
) -> StdResult<MsgChannelOpenInit> {
// retrieve the contract's IBC port
let port_id = deps
.querier
.query_wasm_contract_info(&env.contract.address)?
.ibc_port
.unwrap(); // this is never `None` for contracts with all IBC entrypoints
// prepare the channel open message
Ok(MsgChannelOpenInit {
port_id,
channel: Some(Channel {
state: State::Init.into(),
ordering: Order::Ordered.into(),
counterparty: Some(Counterparty {
port_id: counterparty_port_id,
channel_id: String::new(),
}),
connection_hops: vec![connection_id],
version: IBC_APP_VERSION.to_string(),
upgrade_sequence: 0,
}),
signer: env.contract.address.to_string(),
})
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
deps: DepsMut,
_env: Env,
msg: IbcChannelOpenMsg,
) -> StdResult<IbcChannelOpenResponse> {
match msg {
IbcChannelOpenMsg::OpenInit { channel } => {
// check if channel opening is allowed
ensure!(
ALLOW_CHANNEL_OPENING.load(deps.storage)?,
StdError::generic_err("channel opening is disabled")
);
// disable channel opening again
ALLOW_CHANNEL_OPENING.save(deps.storage, &false)?;
// ...
}
IbcChannelOpenMsg::OpenTry { .. } => { /* ... */ }
}
Ok(None)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
// ...
// prepare the channel open message
let channel_open_init = new_msg_channel_open_init(
deps.as_ref(),
&env,
msg.connection_id,
msg.counterparty_port_id,
)?;
// enable channel opening
ALLOW_CHANNEL_OPENING.save(deps.storage, &true)?;
// send an any message to initiate the channel opening
// Note that you need to use the deprecated `CosmosMsg::Stargate` variant if you need to
// support pre-2.0 chains
Ok(Response::new().add_message(AnyMsg {
type_url: "/ibc.core.channel.v1.MsgChannelOpenInit".into(),
value: channel_open_init.encode_to_vec().into(),
}))
}
pub const ALLOW_CHANNEL_OPENING: Item<bool> = Item::new("allow_opening");
const IBC_APP_VERSION: &str = "my-protocol-v1";
As you can see, the contract itself sends the message to initialize the channel handshake in this example. Please note that the rest of the handshake still needs to be done by a relayer.
Channel connect
After the OpenTry
variant is called on chain B, the relayer calls the ibc_channel_connect
entrypoint, first with the IbcChannelConnectMsg::OpenAck
variant on chain A, then the
IbcChannelConnectMsg::OpenConfirm
variant on chain B. The full data this entrypoint receives can
be seen in the IbcChannelConnectMsg
(opens in a new tab) documentation. Here is more example code:
use cw_storage_plus::Item;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(
deps: DepsMut,
env: Env,
msg: IbcChannelConnectMsg,
) -> StdResult<IbcBasicResponse> {
let channel = msg.channel();
// in this example, we only allow a single channel per contract instance
// you can do more complex checks here
let mut channel_info = CHANNEL.load(deps.storage)?;
ensure!(!channel_info.finalized, StdError::generic_err("channel already finalized"));
debug_assert_eq!(channel_info.channel_id, channel.endpoint.channel_id, "channel ID mismatch");
// at this point, we are finished setting up the channel and can mark it as finalized
channel_info.finalized = true;
CHANNEL.save(deps.storage, &channel_info)?;
Ok(IbcBasicResponse::new())
}
#[cw_serde]
struct ChannelInfo {
channel_id: String,
/// whether the channel is completely set up
finalized: bool,
}
const CHANNEL: Item<ChannelInfo> = Item::new("channel");
Closing a channel
Similarly to opening a channel, closing a channel involves a handshake process. However, this time
the process only involves one call per chain. The relayer initiates the process by calling the
ibc_channel_close
entrypoint with the IbcChannelCloseMsg::CloseInit
variant on chain A, followed
by the same entrypoint with the IbcChannelCloseMsg::CloseConfirm
on chain B. The full data can be
seen in the IbcChannelCloseMsg
(opens in a new tab) documentation. Here is an example:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_close(
deps: DepsMut,
env: Env,
msg: IbcChannelCloseMsg,
) -> StdResult<IbcBasicResponse> {
Err(StdError::generic_err("closing not allowed"))
}
While closing channels is something that is possible in IBC, you should think carefully about whether you want to allow it in your protocol. In many cases, it might be better to keep the channel open and return an error when someone tries to close it.
In this entrypoint, you can handle the closing process as you see fit. This can involve cleaning up any storage related to the channel or simply returning an error to prevent the channel from closing.