Packet lifecycle
In this section, we will cover the lifecycle of a packet in the IBC protocol. Sending and receiving packets is the core functionality of IBC, and it is important to understand how packets are processed and what implications this has for your protocol.
There are two flows that a packet can take during its lifecycle. Both start with a packet being sent on chain A. The first flow is the successful delivery of the packet to chain B:
- Sending a packet: A packet is sent from chain A through a channel.
- Receiving a packet: The packet is received and acknowledged by chain B.
- Receiving a packet acknowledgement: Chain A receives the acknowledgement.
The second flow is when the packet is not relayed to chain B within the specified time frame:
- Sending a packet: A packet is sent from chain A through a channel.
- Receiving a packet timeout: The packet is not received within the specified time frame.
A visual representation of these flows can be found in the IBC specification (opens in a new tab).
Sending a packet
In order to send a packet, you need to send the IbcMsg::SendPacket
message. It looks like this:
use cw_storage_plus::Item;
const CHANNEL: Item<ChannelInfo> = Item::new("channel");
let channel_id = CHANNEL.load(deps.storage)?.channel_id;
// construct the transfer message
let msg = IbcMsg::SendPacket {
channel_id,
data: br#"{"hello":{"text":"Hello, chain B!"}}"#.into(),
timeout: IbcTimeout::with_block(IbcTimeoutBlock {
revision: 4,
height: 1000,
}),
};
// attach the message and return the response
Ok(Response::new().add_message(msg))
The channel_id
is the identifier of the channel you want to use for the
packet. This must be a channel that was previously established, as described in
the previous section, so you probably want to load this from contract state.
The data
field is of type Binary
and contains the actual packet data. This is the information
that you want to send to chain B.
The timeout
field can either be a timestamp or a block height, as measured on the destination
chain. It is used to prevent the packet from being stuck in limbo if the destination chain does not
receive it.
Receiving a packet
To receive a packet, you need to implement the ibc_packet_receive
entrypoint:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
deps: DepsMut,
_env: Env,
msg: IbcPacketReceiveMsg,
) -> StdResult<IbcReceiveResponse> {
let msg: PacketMsg = from_json(msg.packet.data)?;
match msg {
PacketMsg::Hello { text } => {
// here you would do something with the received data
deps.api.debug(&text);
Ok(IbcReceiveResponse::new(StdAck::success(b"Hello, chain A!")))
},
}
}
#[cw_serde]
enum PacketMsg {
Hello { text: String },
}
As you can see, this looks quite similar to the execute
entrypoint. The main difference is that
you need to parse the packet data yourself, because it is not necessarily in JSON format. What is
also different, is the return type. Instead of returning a Response
, you need to return an
IbcReceiveResponse
(opens in a new tab) with an optional acknowledgement (see next section
for more details about not providing an acknowledgement).
In this example, we used the StdAck
(opens in a new tab) type for our acknowledgement. It encodes the acknowledgement
as a JSON object which can either be {"result":"BASE64 OF DATA"}
in the success case, or
{"error":"error string"}
in the error case. This is the same format that is used in some existing
IBC protocols, like e.g. ICS-20 (opens in a new tab). However, using this format is not a requirement. You can use any
type you like, as long as it can be converted to Binary
.
Error handling
Using a different acknowledgement format comes with a caveat, though: The error handling for
ibc_packet_receive
works differently than for other entrypoints.
If you return an error from ibc_packet_receive
, it will revert your contract state changes, but
will not fail the transaction. Instead, it will cause an StdAck::error
acknowledgement to be
written. So, if you want to return errors in ibc_packet_receive
, you might want to also use the
StdAck
(opens in a new tab) type for your successful acknowledgements to keep the format consistent for the sender.
Alternatively, you can also handle all errors within the ibc_packet_receive
entrypoint, always
returning an Ok(...)
result. In that case, state changes will be committed, the transaction
will succeed and you can use whatever acknowledgement format you prefer.
Panicking in ibc_packet_receive
works the same as in other entrypoints: it will revert all state
changes and fail the transaction. One use case for this is implementing a relayer allowlist
because the failed receive can then be picked up by another relayer.
Async acknowledgement
In some cases, you might want to acknowledge a packet asynchronously. This means that you receive
the packet, but you don't immediately return an acknowledgement. One case where this is useful is if
you need to perform some other action that requires more than one transaction to complete before you
can acknowledge. To do that, you can return a IbcReceiveResponse::without_ack()
response, save the
destination channel ID and packet sequence in contract state, and then acknowledge the packet later
using the IbcMsg::WriteAcknowledgement
message.
It is your responsibility to ensure that a received packet is always acknowledged at some point. Not acknowledging can lead to problems for the sender of the packet.
use cw_storage_plus::Map;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
deps: DepsMut,
_env: Env,
msg: IbcPacketReceiveMsg,
) -> StdResult<IbcReceiveResponse> {
// save the data we need for the async acknowledgement in contract state
// note: we are just saving a String here, but you can save any information
// you need for the acknowledgement later
ACK_LATER.save(
deps.storage,
&(msg.packet.sequence, msg.packet.dest.channel_id),
&"ack str".to_string(),
)?;
// return without an acknowledgement
Ok(IbcReceiveResponse::without_ack())
}
/// Called somewhere in the contract to acknowledge the packet
pub fn async_ack(
deps: DepsMut,
env: Env,
info: MessageInfo,
packet_sequence: u64,
channel_id: String,
) -> StdResult<Response> {
// load data from contract state
let ack_str = ACK_LATER.load(deps.storage, &(packet_sequence, channel_id.clone()))?;
// send the acknowledgement
Ok(Response::new().add_message(IbcMsg::WriteAcknowledgement {
packet_sequence,
channel_id,
ack: IbcAcknowledgement::new(StdAck::success(ack_str.as_bytes())),
}))
}
const ACK_LATER: Map<&(u64, String), String> = Map::new("ack_later");
Receiving a packet acknowledgement
After the packet has been received and acknowledged by chain B, the relayer will pass the
acknowledgement back to chain A, resulting in the ibc_packet_ack
entrypoint being called. This is
where you can handle the acknowledgement:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(
_deps: DepsMut,
_env: Env,
msg: IbcPacketAckMsg,
) -> StdResult<IbcBasicResponse> {
// this example assumes that the acknowledgement is an StdAck
let ack: StdAck = from_json(&msg.acknowledgement.data)?;
// here you can do something with the acknowledgement
Ok(IbcBasicResponse::new())
}
The IbcPacketAckMsg
(opens in a new tab) struct contains the acknowledgement and original packet data, as well as the
address of the relayer that sent the acknowledgement.
When this entrypoint is called, it means that the packet has been successfully delivered to chain B and has been processed by the counterparty. This is the happy path of the packet lifecycle.
Receiving a packet timeout
If the packet is not relayed to chain B within the specified time frame (e.g. because the chain is
stopped or no relayer is picking the packet up), a relayer can call the ibc_packet_timeout
entrypoint with a IbcPacketTimeoutMsg
(opens in a new tab). This is where you can handle the timeout.
Please note that a timeout happening on an ordered channel automatically closes the channel, since after a timeout the order of packets can no longer be guaranteed.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_timeout(
_deps: DepsMut,
_env: Env,
_msg: IbcPacketTimeoutMsg,
) -> StdResult<IbcBasicResponse> {
Ok(IbcBasicResponse::new())
}
When this entrypoint is called, it means that the packet has not been delivered until the timeout specified when sending the packet. This is the unhappy path of the packet lifecycle.
Note that the timeout is based on when the packet is received on chain B, not on when it is acknowledged. This means that a packet that is received, but not acknowledged will not trigger a timeout.
State rewinding
As you can see, IBC is inherently non-atomic. In a single atomic transaction, you can do all the changes you want and later return an error, which reverts the whole transaction. In IBC, you send a packet in one transaction and receive the acknowledgement or timeout in another transaction. This means that you have to take care of undoing the changes yourself. This includes any state changes you made when sending the packet, as well as any funds sent to you. You also need to make sure that when you send the packet, you don't do anything that shouldn't happen until the packet is acknowledged (like sending funds to someone else).
For state changes there are two main strategies:
- Temporary state: You can save the state changes you want to make in a special temporary storage container. If the packet is acknowledged, you can move them to the main storage to finalize them. If the packet times out, you can discard them.
- Undoing state changes: You can do the state changes when sending the packet and later undo them if the packet times out. It is important to make sure that you can actually undo the changes you made and that you cannot get into an inconsistent state by doing this. For example, if you give tokens to someone in exchange for sending an IBC packet, you must lock those tokens until the packet is acknowledged. Otherwise, the user could spend them before the packet is acknowledged and you would not be able to undo the transfer.
You can mix and match these strategies as needed, but be especially careful with the second one. It is easy to make logical errors that can lead to problems.