IBC allows a ledger to track another ledger's consensus state using a light client. IBC is a protocol to agree the consensus state and to send/receive packets between ledgers.
We have mainly two components for IBC integration, IBC hander and IBC validity predicate. IBC hander is a set of functions to handle IBC-related data. A transaction calls these functions for IBC operations. IBC validity predicate is a native validity predicate to validate the transaction which mutates IBC-related data.
Its storage key should be prefixed with InternalAddress::Ibc
to differ them from other storage operations. A path after the prefix specifies an IBC-related data. The paths are defined by ICS 24. The utility functions for the keys are defined here. For example, a client state of a counterparty ledger will be stored with a storage key #IBC_encoded_addr/clients/{client_id}/clientState
. The IBC transaction and IBC validity predicate can use the storage keys to read/write IBC-related data according to IBC protocol.
A requester (IBC relayer or user) who wants to execute IBC operations on a ledger should make a transaction and an IBC message as transaction data, and submit a transaction with the transaction data. We provide tx_ibc.wasm
for IBC transaction.
The transaction can mutate the ledger state by writing not only data specified in the transaction but also IBC-related data on the storage sub-space. It depends on the given IBC message. The dispatch
function will check the message and call the corresponding IBC operations. Also, it emits an IBC event at the end of the transaction.
IBC transaction mutates the ledger state. We provides IBC operations, e.g. create_client
. Basically, they read IBC-related data, check and update them according to IBC protocol. For example, when it receives a message MsgCreateAnyClient
to create a new client for the counterparty chain, the transaction increments the client counter, makes a new client ID and inserts the client type, the client state and the client consensus state, then emits an event with the client ID. The transaction accesses the storage through the host environment functions.
The ledger should set an IBC event to events
in the ABCI response to allow relayers to get the events.
TxResult
can have IbcEvent
. IbcEvent
should have the IBC event type and necessary data according to the IBC operation. A transaction sets IbcEvent
on the write log as non-committed data with the host environment function tx_emit_ibc_event
. When the block is finalized, the event is given to the ABCI response.
IBC relayer can get these events by subscribing to the ledger with Tendermint RPC or getting the response after the relayer submits a transaction. It is parsed in the relayer by from_tx_response_event()
.
IBC validity predicate is invoked after the transaction execution to validate the IBC operations. The trigger invoking IBC validity predicate is changing IBC-related data whose key is prefixed with #IBC-encoded-addr
.
IBC validity predicate validates that the state changes by the IBC transaction are valid by checking the ledger state including prior and posterior. If the validation succeeds, the state changes are committed and the event is emitted. If the validation fails, the IBC-related mustations are dropped and the event isn't emitted. For the performance, IBC validity predicate is a native validity predicate that are built into the ledger.
IBC validity predicate has to execute the following validations for state changes of IBC-related data. validate_tx
calls the corresponding validation functions according to the prefix of each storage key.
/* shared/src/ledger/ibc/vp/mod.rs */
/// IBC VP
pub struct Ibc<'a, DB, H, CA>
where
DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>,
H: StorageHasher,
CA: 'static + WasmCacheAccess,
{
/// Context to interact with the host structures.
pub ctx: Ctx<'a, DB, H, CA>,
}
impl NativeVp for Ibc {
fn validate_tx(
&self,
tx_data: &[u8],
keys_changed: &BTreeSet<Key>,
_verifiers: &BTreeSet<Address>,
) -> Result<bool> {
for key in keys_changed {
// Check the prefix of the storage key
if let Some(ibc_prefix) = ibc_prefix(key) {
match ibc_prefix {
IbcPrefix::Client => {
if is_client_counter_key(key) {
// Check if the counter is incremented
} else {
let client_id = get_client_id(key);
// client validation
self.validate_client(&client_id, tx_data)?
}
}
IbcPrefix::Connection => {
self.validate_connection(key, tx_data)?
}
IbcPrefix::Channel => {
self.validate_channel(key, tx_data)?
}
IbcPrefix::Port => self.validate_port(key)?,
IbcPrefix::Capability => self.validate_capability(key)?,
IbcPrefix::SeqSend => {
self.validate_sequence_send(key, tx_data)?
}
IbcPrefix::SeqRecv => {
self.validate_sequence_recv(key, tx_data)?
}
IbcPrefix::SeqAck => {
self.validate_sequence_ack(key, tx_data)?
}
IbcPrefix::Commitment => {
self.validate_commitment(key, tx_data)?
}
IbcPrefix::Receipt => {
self.validate_receipt(key, tx_data)?
}
IbcPrefix::Ack => self.validate_ack(key)?,
IbcPrefix::Event => {}
IbcPrefix::Unknown => {
return Err(Error::KeyError(format!(
"Invalid IBC-related key: {}",
key
)));
}
}
}
}
Ok(true)
}
The IBC client is validated by checking the state change for updating or upgrading. ibc-rs
provides the check functions for the updating and upgrading.
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_client(
&self,
client_id: &ClientId,
tx_data: &[u8],
) -> Result<()> {
// Check the client state change
match self.get_client_state_change(client_id)? {
StateChange::Created => {
// "CreateClient"
// Confirm that the corresponding consensus state exists
}
StateChange::Updated => {
// check the message
let ibc_msg = IbcMessage::decode(tx_data)?;
match ibc_msg.0 {
Ics26Envelope::Ics2Msg(ClientMsg::UpdateClient(msg)) => {
// Check the header, the updated client state and consensus state
// - Refer to `ibc-rs::ics02_client::client_def::check_header_and_update_state()`
}
Ics26Envelope::Ics2Msg(ClientMsg::UpgradeClient(msg)) => {
// Check the upgraded client state and consensus state
// - Refer to `ibc-rs::ics02_client::client_def::verify_upgrade_and_update_state()`
}
_ => Err(Error::InvalidStateChange(format!(
"The state change of the client is invalid: ID {}",
client_id
))),
}
}
_ => Err(Error::InvalidStateChange(format!(
"The state change of the client is invalid: ID {}",
client_id
))),
}
}
}
The IBC connection is validated by checking the state change for creating or updating in connection handshake. Some validations requires proof verification to verify the counterparty state. ibc-rs
provides the proof verification function for the counterparty connection end.
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_connection(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
if is_connection_counter_key(key) {
// Check if the counter is incremented
return Ok(());
}
// Check if the connection end exists
let conn_id = connection_id(key)?;
let conn = self.connection_end(&conn_id).map_err(|_| {
Error::InvalidConnection(format!(
"The connection doesn't exist: ID {}",
conn_id
))
})?;
// Check the state change
match self.get_connection_state_change(&conn_id)? {
StateChange::Created => {
match conn.state() {
State::Init => {
// "ConnectionOpenInit"
// Confirm that the corresponding client exists
}
State::TryOpen => {
// "ConnectionOpenTry"
// Confirm that the version is compatible
// Verify the proofs to check if the counterparty connection end exists as expected
// - Use `ibc-rs::ics03_connection::handler::verify::verify_proofs()`
}
}
}
StateChange::Updated => {
if conn.state() != State::Open {
return Err(Error::InvalidConnection(format!(
"The state of the connection is invalid: ID {}",
conn_id
)));
}
// Check the previous state of the connection end
let prev_conn = self.connection_end_pre(conn_id)?;
match prev_conn.state() {
State::Init => {
// "ConnectionOpenAck"
// Verify the proofs to check if the counterparty connection end exists as expected
// - Use `ibc-rs::ics03_connection::handler::verify::verify_proofs()`
}
State::TryOpen => {
// "ConnectionOpenConfirm"
// Verify the proofs to check if the counterparty connection end exists as expected
// - Use `ibc-rs::ics03_connection::handler::verify::verify_proofs()`
}
}
}
_ => Err(Error::InvalidStateChange(format!(
"The state change of the connection is invalid: ID {}",
conn_id
))),
}
}
}
The IBC port and channel end are validated by checking the state change for creating or updating in channel handshake as the connection validation. The validation function also checks the state change when the channel end is closed unlike a connection end. When packet timeout, the validation has to confirm that the commitment has been deleted. This deletion will trigger the packet validation for the timed-out packet.
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_port(&self, key: &Key) -> Result<()> {
let port_id = port_id(key)?;
match self.get_port_state_change(&port_id)? {
StateChange::Created => {
// Confirm that the port is owend
}
StateChange::Updated => {
// Check the port is bound to another channel or released properly
}
_ => Err(Error::InvalidPort(format!(
"The state change of the port is invalid: Port {}",
port_id
))),
}
}
}
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_channel(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
if is_channel_counter_key(key) {
// Check if the counter is incremented
return Ok(());
}
// Confirm that the port is owend
// Confirm that the version is compatible
// Check the channel state change
match self.get_channel_state_change(&port_channel_id)? {
StateChange::Created => match channel.state() {
State::Init => {
// "ChannelOpenInit"
}
State::TryOpen => {
// "ChannelOpenTry"
// Verify the proof to check if the counterparty channel end exists as expected
// - Use `ibc-rs::ics04_connection::handler::verify::verify_channel_proofs()`
}
_ => Err(Error::InvalidChannel(format!(
"The channel state is invalid: Port/Channel {}, State {}",
port_channel_id,
channel.state()
))),
},
StateChange::Updated => {
let prev_channel = self.channel_end_pre(port_channel_id)?;
match channel.state() {
State::Open => match prev_channel.state() {
State::Init => {
// "ChannelOpenAck"
// Verify the proof to check if the counterparty channel end exists as expected
// - Use `ibc-rs::ics04_connection::handler::verify::verify_channel_proofs()`
}
State::TryOpen => {
// "ChannelOpenConfirm"
// Verify the proof to check if the counterparty channel end exists as expected
// - Use `ibc-rs::ics04_connection::handler::verify::verify_channel_proofs()`
}
}
State::Closed => {
// Confirm that the previous state is State::Open
// Check the message
let ibc_msg = IbcMessage::decode(tx_data)?;
match ibc_msg.0 {
Ics26Envelope::Ics4PacketMsg(PacketMsg::ToPacket(msg)) => {
// "TimeoutPacket"
// Confirm that the commitment has been deleted
// - The state change of the commitment triggers the packet validation
}
Ics26Envelope::Ics4PacketMsg(PacketMsg::ToClosePacket(
msg,
)) => {
// "TimeoutOnClosePacket"
// Confirm that the commitment has been deleted
// - The state change of the commitment triggers the packet validation
}
Ics26Envelope::Ics4ChannelMsg(
ChannelMsg::ChannelCloseInit(msg),
) => {
// "ChannelCloseInit"
}
Ics26Envelope::Ics4ChannelMsg(
ChannelMsg::ChannelCloseConfirm(msg),
) => {
// "ChannelCloseConfirm"
// Verify the proof to check if the counterparty channel end exists as expected
// - Use `ibc-rs::ics04_connection::handler::verify::verify_channel_proofs()`
}
_ => Err(Error::InvalidMessage(format!(
"The state change of the channel is invalid for the message: \
Port/Channel {}",
port_channel_id,
))),
}
}
}
}
_ => Err(Error::InvalidStateChange(format!(
"The state change of the channel: Port/Channel {}",
port_channel_id
))),
}
}
}
When the packet sending, receiving, or acknowledgement, the sequence counter is incremented. The sequence validation checks these sequences and the existence of the commitment, the receipt, or acknowledgement. The contents of them are validated in the packet validation functions.
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_sequence_send(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
// Confirm that the sequence is incremented
// Confirm that the commitment exists
}
pub(super) fn validate_sequence_recv(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
// Confirm that the sequence is incremented
// Confirm that the receipt and the acknowledgement exist
}
pub(super) fn validate_sequence_ack(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
// Confirm that the sequence is incremented
// Confirm that the commitment doesn't exist
}
}
The validation functions should check commitment, receipt, and acknowledgement which are stored or deleted when packet sending, receiving or acknowledgement. And, when the commitment has been deleted, the packet timeout should be checked.
impl<'a, DB, H, CA> Ibc<'a, DB, H, CA>
where
DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>,
H: 'static + StorageHasher,
CA: 'static + WasmCacheAccess,
{
pub(super) fn validate_commitment(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
// check if the commitment is stored or deleted
match self
.get_state_change(key)
.map_err(|e| Error::InvalidStateChange(e.to_string()))?
{
StateChange::Created => {
// "SendPacket"
// Confirm that the channel is open
// Validate the sent packet
// Confirm that the expected commitment exists
}
StateChange::Deleted => {
let ibc_msg = IbcMessage::decode(tx_data)?;
match channel.state() {
State::Open => {
match &ibc_msg.0 {
Ics26Envelope::Ics4PacketMsg(
PacketMsg::AckPacket(msg),
) => {
// "PacketAcknowledgement"
// Verify the proof to check if the expected ack exists on the counterparty
}
Ics26Envelope::Ics4PacketMsg(
PacketMsg::ToPacket(_),
)
| Ics26Envelope::Ics4PacketMsg(
PacketMsg::ToClosePacket(_),
) => {
// "PacketTimeout"
// Confirm that the deleted commitment was for the channel and the counterparty
if !is_timed_out(packet) {
// "PacketTimedoutOnClose"
// Verify the proof to check if the counterparty channel is closed
}
}
_ => Err(Error::InvalidChannel(format!(
"The channel state is invalid: Port {}, \
Channel {}",
commitment_key.0, commitment_key.1
))),
}
}
State::Closed => {
// "PacketTimeout"
// Confirm that the deleted commitment was for the channel and the counterparty
// Confirm that the packet timed out
}
_ => Err(Error::InvalidChannel(format!(
"The channel state is invalid: Port {}, Channel {}",
commitment_key.0, commitment_key.1
))),
}
}
_ => Err(Error::InvalidStateChange(format!(
"The state change of the commitment is invalid: Key {}",
key
))),
}
}
}
}
pub(super) fn validate_receipt(
&self,
key: &Key,
tx_data: &[u8],
) -> Result<()> {
// Check the state change of the receipt
match self
.get_state_change(key)
.map_err(|e| Error::InvalidStateChange(e.to_string()))?
{
StateChange::Created => {
OK(())
}
_ => Err(Error::InvalidStateChange(
"The state change of the receipt is invalid".to_owned(),
)),
}
}
pub(super) fn validate_ack(&self, key: &Key) -> Result<()> {
match self
.get_state_change(key)
.map_err(|e| Error::InvalidStateChange(e.to_string()))?
{
StateChange::Created => {
// Confirm that the receipt exists
}
_ => Err(Error::InvalidStateChange(
"The state change of the acknowledgment is invalid".to_owned(),
)),
}
}
}
A query for a proven IBC-related data returns the value and the proof. The proof is used to verify if the key-value pair exists or doesn't exist on the counterpart ledger in IBC validity predicate (ICS 23).
The query response has the proof as tendermint::merkle::proof::Proof
, which consists of a vector of tendermint::merkle::proof::ProofOp
. ProofOp
should have data
, which is encoded to Vec<u8>
from ibc_proto::ics23::CommitmentProof
. The relayer getting the proof converts the proof from tendermint::merkle::proof::Proof
to ibc::ics23_commitment::commitment::CommitmentProofBytes
by convert_tm_to_ics_merkle_proof()
and sets it to the request data of the following IBC operation.
IBC relayer monitors the ledger, gets the status, state and proofs on the ledger, and requests transactions to the ledger via Tendermint RPC according to IBC protocol. For relayers, the ledger has to make a packet, emits an IBC event and stores proofs if needed. And, a relayer has to support Namada ledger to query and validate the ledger state. It means that ChainEndpoint
in IBC Relayer of ibc-rs should be implemented for Anoma like that of CosmosSDK. As those of Cosmos, these querys can request ABCI queries to Namada.
impl ChainEndpoint for Namada {
...
}
IBC token VP checks a transfer of a sent/received/refunded token over IBC. These transfers change the state of accounts sub-prefixed with IbcToken
including a hash or the owner address is IbcEscrow
, IbcBurn
, IbcMint
, i.e. this VP is triggered by the state change of these accounts.
The existing token's VP vp_token
checks if the total of the changes in the transaction should be zero. IBC-related accounts should be included as general accounts.
IBC token VP as a native VP should check if the escrow/unescrow/burn/mint has been done properly in the transaction. When an IBC transaction has MsgTransfer
, the VP should check if the amount of the specified token by the message has been escrowed or burned. When an IBC transaction has MsgRecvPacket
with FungibleTokenPacketData
, the VP should check if the amount of the specified token by the data has been unescrowed or minted. For example, if a transaction tries to unescrow an amount and to send it to a different account from the specified account, the VP should refuse it, even if the transaction satisfies IBC protocol.
In a transaction with MsgTransfer
(defined in ibc-rs) including FungibleTokenPacketData
as transaction data, the specified token is sent according to ICS20 and a packet is sent.
The transaction updates the sender's balance by escrowing or burning the amount of the token. The account, the sent token(denomination), and the amount are specified by MsgTransfer
. The denomination field would indicate that this chain is the source zone or the sink zone.
Basically, the sender key is {token_addr}/balance/{sender_addr}
. {token_addr}
and {sender_addr}
is specified by FungibleTokenPacketData
. When the denomination {denom}
in FungibleTokenPacketData
specifies the source chain, the transfer operation is executed from the origin-specific account {token_addr}/ibc/{ibc_token_hash}/balance/{sender_addr}
(Ref. Receiver). We can set {token_addr}
, {port_id}/{channel_id}/../{token_addr}
, or ibc/{ibc_token_hash}/{token_addr}
to the denomination. When ibc/{ibc_token_hash}/
is prefixed, the transfer looks up the prefixed denomination {port_id}/{channel_id}/{denom}
by the {ibc_token_hash}
. {denom}
might have more prefixes to specify the source chains, e.g. {port_id_b}/{channel_id_b}/{port_id_a}/{channel_id_a}/{token_addr}
. Accoding to the prefixed port ID and channel ID, the transfer operation escrows or burns the amount of the token (ICS20).
When this chain is the source zone, i.e. the denomination does NOT start with the port ID and the channel ID of this chain, the amount of the specified token is sent from the sender's account key to the escrow key {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcEscrow
. The escrow address should be associated with IBC port ID and channel ID to unescrow it later. The escrow address is one of internal addresses, InternalAddress::IbcEscrow
. It is not allowed to transfer from the escrow account without IBC token transfer operation. IBC token VP should check the transfer from the escrow accounts.
When the destination chain was the source, i.e. the denomination starts with the port ID and the channel ID of this chain, the amount of the specified token is sent from the sender's account to a key {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcBurn
. IbcBurn
is one of internal addresses, InternalAddress::IbcBurn
. The value of the key should NOT written to the block when the block is committed, i.e. reading the previous value of the key in a VP results in always None
and the balance is zero by default. We can use tx_write_temp
in the transaction for these writes.
In a transaction with MsgRecvPacket
(defined in ibc-rs) including FungibleTokenTransferData
as transaction data, the specified token is received according to ICS20.
The transaction updates the receiver's balance by unescrowing or minting the amount of the token. The account(receiver), the received token(denomination), and the amount are specified by FungibleTokenPacketData
in the received packet.
The receiver's account key should be origin-specific because the token should be returned to the source chain if needed. The key is {token_addr}/ibc/{ibc_token_hash}/balance/{receiver_addr}
. {ibc_token_hash}
is calculated from the denomination prefixed with this chain's port ID and chain ID, and the token address. And, the original denomination should be persistent with the storage key #IBC_encoded_addr/denom/{ibc_token_hash}
for looking it up when sending the received token.
When this chain was the source zone, i.e. the denomination starts with this chain's port ID and channel ID, the amount of the token is sent from its escrow key {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcEscrow
.
When this chain is not the source zone, i.e. the denomination does NOT start with this chain's port ID and channel ID, the amount of the token is minted from the mint account {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcMint
. The IbcMint
is one of internal addresses, InternalAddress::IbcMint
. The account is NOT updated when the block is committed same as IbcBurn
, i.e. reading the previous value of the mint account in a VP results in always the maximum amount.
When a packet has timed out or a failure acknowledgement is given, the escrowed or burned amount of the token should be refunded by unescrowing or minting the amount of the token on the chain which has sent the token. i.e. the IBC transaction should transfer the amount of the token from {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcEscrow
or {token_addr}/ibc/{port_id}/{channel_id}/balance/IbcMint
to the sender account.