synor/crates/synor-ibc/src/channel.rs
Gulshan Yadav 6037695afb feat(ibc): add Phase 14 Milestone 1 - Cross-Chain IBC Interoperability
Implements full Inter-Blockchain Communication (IBC) protocol:

synor-ibc crate (new):
- Light client management (create, update, verify headers)
- Connection handshake (4-way: Init, Try, Ack, Confirm)
- Channel handshake (4-way: Init, Try, Ack, Confirm)
- Packet handling (send, receive, acknowledge, timeout)
- Merkle commitment proofs for state verification
- ICS-20 fungible token transfer support
- Atomic swap engine with HTLC (hashlock + timelock)

IBC Bridge Contract (contracts/ibc-bridge):
- Token locking/unlocking for cross-chain transfers
- Relayer whitelist management
- Channel registration and sequence tracking
- HTLC atomic swap (create, claim, refund)
- Event emission for indexing
- 52KB optimized WASM binary

Test coverage: 40 tests passing
2026-01-19 16:51:59 +05:30

768 lines
21 KiB
Rust

//! IBC Channel Management
//!
//! Channels provide the transport layer for IBC packets. Each channel
//! is bound to a specific port and connection.
//!
//! # Channel Types
//!
//! - **Ordered**: Packets must be received in order (sequence numbers)
//! - **Unordered**: Packets can be received in any order
//!
//! # Channel Handshake (4-way)
//!
//! Similar to connection handshake:
//! 1. ChanOpenInit
//! 2. ChanOpenTry
//! 3. ChanOpenAck
//! 4. ChanOpenConfirm
use crate::connection::ConnectionId;
use crate::error::{IbcError, IbcResult};
use crate::types::Height;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
/// Port identifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct PortId(pub String);
impl PortId {
/// Create a new port ID
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
/// Transfer port
pub fn transfer() -> Self {
Self("transfer".to_string())
}
/// Interchain accounts host port
pub fn ica_host() -> Self {
Self("icahost".to_string())
}
/// Interchain accounts controller port
pub fn ica_controller(owner: &str) -> Self {
Self(format!("icacontroller-{}", owner))
}
/// Validate port ID
pub fn validate(&self) -> IbcResult<()> {
if self.0.is_empty() {
return Err(IbcError::InvalidIdentifier("port ID cannot be empty".to_string()));
}
if self.0.len() > 128 {
return Err(IbcError::InvalidIdentifier("port ID too long".to_string()));
}
// Alphanumeric and limited special characters
if !self.0.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
return Err(IbcError::InvalidIdentifier(
"port ID contains invalid characters".to_string(),
));
}
Ok(())
}
}
impl fmt::Display for PortId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Channel identifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct ChannelId(pub String);
impl ChannelId {
/// Create a new channel ID
pub fn new(sequence: u64) -> Self {
Self(format!("channel-{}", sequence))
}
/// Parse sequence from ID
pub fn sequence(&self) -> Option<u64> {
self.0.strip_prefix("channel-")?.parse().ok()
}
}
impl fmt::Display for ChannelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Channel ordering
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelOrder {
/// Packets can arrive in any order
Unordered,
/// Packets must arrive in sequence order
Ordered,
}
impl ChannelOrder {
/// Get order name
pub fn as_str(&self) -> &'static str {
match self {
ChannelOrder::Unordered => "ORDER_UNORDERED",
ChannelOrder::Ordered => "ORDER_ORDERED",
}
}
}
impl fmt::Display for ChannelOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Channel state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelState {
/// Uninitialized (default)
Uninitialized,
/// Init: ChanOpenInit sent
Init,
/// TryOpen: ChanOpenTry sent
TryOpen,
/// Open: Channel fully established
Open,
/// Closed: Channel closed
Closed,
}
impl ChannelState {
/// Get state name
pub fn as_str(&self) -> &'static str {
match self {
ChannelState::Uninitialized => "UNINITIALIZED",
ChannelState::Init => "INIT",
ChannelState::TryOpen => "TRYOPEN",
ChannelState::Open => "OPEN",
ChannelState::Closed => "CLOSED",
}
}
}
impl fmt::Display for ChannelState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Channel counterparty
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelCounterparty {
/// Counterparty port ID
pub port_id: PortId,
/// Counterparty channel ID
pub channel_id: Option<ChannelId>,
}
impl ChannelCounterparty {
/// Create a new counterparty
pub fn new(port_id: PortId, channel_id: Option<ChannelId>) -> Self {
Self { port_id, channel_id }
}
}
/// Channel end state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
/// Channel state
pub state: ChannelState,
/// Channel ordering
pub ordering: ChannelOrder,
/// Counterparty information
pub counterparty: ChannelCounterparty,
/// Connection hops (usually single connection)
pub connection_hops: Vec<ConnectionId>,
/// Channel version (app-specific)
pub version: String,
}
impl Channel {
/// Create a new channel in Init state
pub fn new_init(
ordering: ChannelOrder,
counterparty: ChannelCounterparty,
connection_hops: Vec<ConnectionId>,
version: String,
) -> Self {
Self {
state: ChannelState::Init,
ordering,
counterparty,
connection_hops,
version,
}
}
/// Create a new channel in TryOpen state
pub fn new_try_open(
ordering: ChannelOrder,
counterparty: ChannelCounterparty,
connection_hops: Vec<ConnectionId>,
version: String,
) -> Self {
Self {
state: ChannelState::TryOpen,
ordering,
counterparty,
connection_hops,
version,
}
}
/// Check if channel is open
pub fn is_open(&self) -> bool {
self.state == ChannelState::Open
}
/// Check if channel is closed
pub fn is_closed(&self) -> bool {
self.state == ChannelState::Closed
}
/// Set state to TryOpen
pub fn set_try_open(&mut self) {
self.state = ChannelState::TryOpen;
}
/// Set state to Open
pub fn set_open(&mut self) {
self.state = ChannelState::Open;
}
/// Set state to Closed
pub fn set_closed(&mut self) {
self.state = ChannelState::Closed;
}
/// Set counterparty channel ID
pub fn set_counterparty_channel_id(&mut self, id: ChannelId) {
self.counterparty.channel_id = Some(id);
}
/// Validate the channel
pub fn validate(&self) -> IbcResult<()> {
if self.connection_hops.is_empty() {
return Err(IbcError::InvalidChannelState {
expected: "at least one connection hop".to_string(),
actual: "none".to_string(),
});
}
self.counterparty.port_id.validate()?;
Ok(())
}
}
/// Channel key (port, channel)
pub type ChannelKey = (PortId, ChannelId);
/// Sequence counters for a channel
#[derive(Debug, Clone, Default)]
pub struct ChannelSequences {
/// Next sequence to send
pub next_send: u64,
/// Next sequence to receive
pub next_recv: u64,
/// Next sequence to acknowledge
pub next_ack: u64,
}
/// Channel manager
pub struct ChannelManager {
/// Channels by (port, channel)
channels: HashMap<ChannelKey, Channel>,
/// Sequence counters by (port, channel)
sequences: HashMap<ChannelKey, ChannelSequences>,
/// Port bindings (port -> module name)
port_bindings: HashMap<PortId, String>,
/// Next channel sequence per port
next_channel_sequence: HashMap<PortId, u64>,
}
impl ChannelManager {
/// Create a new channel manager
pub fn new() -> Self {
Self {
channels: HashMap::new(),
sequences: HashMap::new(),
port_bindings: HashMap::new(),
next_channel_sequence: HashMap::new(),
}
}
/// Bind a port to a module
pub fn bind_port(&mut self, port_id: PortId, module: String) -> IbcResult<()> {
port_id.validate()?;
if self.port_bindings.contains_key(&port_id) {
return Err(IbcError::PortAlreadyBound(port_id.to_string()));
}
self.port_bindings.insert(port_id, module);
Ok(())
}
/// Release a port binding
pub fn release_port(&mut self, port_id: &PortId) -> IbcResult<()> {
if self.port_bindings.remove(port_id).is_none() {
return Err(IbcError::PortNotBound(port_id.to_string()));
}
Ok(())
}
/// Check if port is bound
pub fn is_port_bound(&self, port_id: &PortId) -> bool {
self.port_bindings.contains_key(port_id)
}
/// Generate next channel ID for a port
fn next_channel_id(&mut self, port_id: &PortId) -> ChannelId {
let seq = self.next_channel_sequence.entry(port_id.clone()).or_insert(0);
let id = ChannelId::new(*seq);
*seq += 1;
id
}
/// Get a channel
pub fn get_channel(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<&Channel> {
self.channels
.get(&(port_id.clone(), channel_id.clone()))
.ok_or_else(|| IbcError::ChannelNotFound {
port: port_id.to_string(),
channel: channel_id.to_string(),
})
}
/// Get mutable channel
fn get_channel_mut(
&mut self,
port_id: &PortId,
channel_id: &ChannelId,
) -> IbcResult<&mut Channel> {
self.channels
.get_mut(&(port_id.clone(), channel_id.clone()))
.ok_or_else(|| IbcError::ChannelNotFound {
port: port_id.to_string(),
channel: channel_id.to_string(),
})
}
/// Get sequences for a channel
pub fn get_sequences(&self, port_id: &PortId, channel_id: &ChannelId) -> ChannelSequences {
self.sequences
.get(&(port_id.clone(), channel_id.clone()))
.cloned()
.unwrap_or_default()
}
/// Increment send sequence and return previous value
pub fn increment_send_sequence(
&mut self,
port_id: &PortId,
channel_id: &ChannelId,
) -> u64 {
let key = (port_id.clone(), channel_id.clone());
let seq = self.sequences.entry(key).or_default();
let current = seq.next_send;
seq.next_send += 1;
current
}
/// Increment receive sequence
pub fn increment_recv_sequence(&mut self, port_id: &PortId, channel_id: &ChannelId) {
let key = (port_id.clone(), channel_id.clone());
let seq = self.sequences.entry(key).or_default();
seq.next_recv += 1;
}
/// Increment ack sequence
pub fn increment_ack_sequence(&mut self, port_id: &PortId, channel_id: &ChannelId) {
let key = (port_id.clone(), channel_id.clone());
let seq = self.sequences.entry(key).or_default();
seq.next_ack += 1;
}
/// Channel Open Init
pub fn chan_open_init(
&mut self,
port_id: PortId,
ordering: ChannelOrder,
connection_hops: Vec<ConnectionId>,
counterparty_port: PortId,
version: String,
) -> IbcResult<ChannelId> {
// Validate port
port_id.validate()?;
counterparty_port.validate()?;
// Check port is bound
if !self.is_port_bound(&port_id) {
return Err(IbcError::PortNotBound(port_id.to_string()));
}
// Generate channel ID
let channel_id = self.next_channel_id(&port_id);
// Create channel
let counterparty = ChannelCounterparty::new(counterparty_port, None);
let channel = Channel::new_init(ordering, counterparty, connection_hops, version);
channel.validate()?;
// Store channel
let key = (port_id.clone(), channel_id.clone());
self.channels.insert(key.clone(), channel);
self.sequences.insert(key, ChannelSequences {
next_send: 1,
next_recv: 1,
next_ack: 1,
});
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel init complete"
);
Ok(channel_id)
}
/// Channel Open Try
pub fn chan_open_try(
&mut self,
port_id: PortId,
ordering: ChannelOrder,
connection_hops: Vec<ConnectionId>,
counterparty_port: PortId,
counterparty_channel: ChannelId,
version: String,
_counterparty_version: String,
proof_init: Vec<u8>,
_proof_height: Height,
) -> IbcResult<ChannelId> {
// Validate
port_id.validate()?;
// Check port is bound
if !self.is_port_bound(&port_id) {
return Err(IbcError::PortNotBound(port_id.to_string()));
}
// Verify proof (simplified)
if proof_init.is_empty() {
return Err(IbcError::MissingProof("proof_init required".to_string()));
}
// Generate channel ID
let channel_id = self.next_channel_id(&port_id);
// Create channel
let counterparty = ChannelCounterparty::new(counterparty_port, Some(counterparty_channel));
let channel = Channel::new_try_open(ordering, counterparty, connection_hops, version);
channel.validate()?;
// Store channel
let key = (port_id.clone(), channel_id.clone());
self.channels.insert(key.clone(), channel);
self.sequences.insert(key, ChannelSequences {
next_send: 1,
next_recv: 1,
next_ack: 1,
});
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel try open complete"
);
Ok(channel_id)
}
/// Channel Open Ack
pub fn chan_open_ack(
&mut self,
port_id: &PortId,
channel_id: &ChannelId,
counterparty_channel: ChannelId,
counterparty_version: String,
proof_try: Vec<u8>,
_proof_height: Height,
) -> IbcResult<()> {
let channel = self.get_channel_mut(port_id, channel_id)?;
// Verify state
if channel.state != ChannelState::Init {
return Err(IbcError::InvalidChannelState {
expected: ChannelState::Init.to_string(),
actual: channel.state.to_string(),
});
}
// Verify proof (simplified)
if proof_try.is_empty() {
return Err(IbcError::MissingProof("proof_try required".to_string()));
}
// Update channel
channel.set_counterparty_channel_id(counterparty_channel);
channel.version = counterparty_version;
channel.set_open();
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel ack complete - channel OPEN"
);
Ok(())
}
/// Channel Open Confirm
pub fn chan_open_confirm(
&mut self,
port_id: &PortId,
channel_id: &ChannelId,
proof_ack: Vec<u8>,
_proof_height: Height,
) -> IbcResult<()> {
let channel = self.get_channel_mut(port_id, channel_id)?;
// Verify state
if channel.state != ChannelState::TryOpen {
return Err(IbcError::InvalidChannelState {
expected: ChannelState::TryOpen.to_string(),
actual: channel.state.to_string(),
});
}
// Verify proof (simplified)
if proof_ack.is_empty() {
return Err(IbcError::MissingProof("proof_ack required".to_string()));
}
// Update state
channel.set_open();
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel confirm complete - channel OPEN"
);
Ok(())
}
/// Channel Close Init
pub fn chan_close_init(&mut self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<()> {
let channel = self.get_channel_mut(port_id, channel_id)?;
if !channel.is_open() {
return Err(IbcError::InvalidChannelState {
expected: ChannelState::Open.to_string(),
actual: channel.state.to_string(),
});
}
channel.set_closed();
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel close init - channel CLOSED"
);
Ok(())
}
/// Channel Close Confirm
pub fn chan_close_confirm(
&mut self,
port_id: &PortId,
channel_id: &ChannelId,
proof_init: Vec<u8>,
_proof_height: Height,
) -> IbcResult<()> {
let channel = self.get_channel_mut(port_id, channel_id)?;
if channel.is_closed() {
return Ok(()); // Already closed
}
// Verify proof (simplified)
if proof_init.is_empty() {
return Err(IbcError::MissingProof("proof_init required".to_string()));
}
channel.set_closed();
tracing::info!(
port = %port_id,
channel = %channel_id,
"Channel close confirm - channel CLOSED"
);
Ok(())
}
/// Get all channels
pub fn all_channels(&self) -> impl Iterator<Item = (&ChannelKey, &Channel)> {
self.channels.iter()
}
/// Get channels for a port
pub fn port_channels(&self, port_id: &PortId) -> Vec<(ChannelId, Channel)> {
self.channels
.iter()
.filter(|((p, _), _)| p == port_id)
.map(|((_, c), ch)| (c.clone(), ch.clone()))
.collect()
}
}
impl Default for ChannelManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_id() {
let port = PortId::transfer();
assert_eq!(port.to_string(), "transfer");
assert!(port.validate().is_ok());
let invalid = PortId::new("");
assert!(invalid.validate().is_err());
}
#[test]
fn test_channel_id() {
let id = ChannelId::new(5);
assert_eq!(id.to_string(), "channel-5");
assert_eq!(id.sequence(), Some(5));
}
#[test]
fn test_port_binding() {
let mut manager = ChannelManager::new();
let port = PortId::transfer();
assert!(manager.bind_port(port.clone(), "transfer".to_string()).is_ok());
assert!(manager.is_port_bound(&port));
// Can't bind same port twice
assert!(manager.bind_port(port.clone(), "other".to_string()).is_err());
// Release and rebind
assert!(manager.release_port(&port).is_ok());
assert!(!manager.is_port_bound(&port));
}
#[test]
fn test_channel_handshake() {
let mut manager_a = ChannelManager::new();
let mut manager_b = ChannelManager::new();
let port = PortId::transfer();
let conn = ConnectionId::new(0);
// Bind ports
manager_a.bind_port(port.clone(), "transfer".to_string()).unwrap();
manager_b.bind_port(port.clone(), "transfer".to_string()).unwrap();
// Chain A: Init
let chan_a = manager_a
.chan_open_init(
port.clone(),
ChannelOrder::Unordered,
vec![conn.clone()],
port.clone(),
"ics20-1".to_string(),
)
.unwrap();
// Chain B: TryOpen
let chan_b = manager_b
.chan_open_try(
port.clone(),
ChannelOrder::Unordered,
vec![conn.clone()],
port.clone(),
chan_a.clone(),
"ics20-1".to_string(),
"ics20-1".to_string(),
vec![1, 2, 3],
Height::from_height(100),
)
.unwrap();
// Chain A: Ack
manager_a
.chan_open_ack(
&port,
&chan_a,
chan_b.clone(),
"ics20-1".to_string(),
vec![1, 2, 3],
Height::from_height(101),
)
.unwrap();
assert!(manager_a.get_channel(&port, &chan_a).unwrap().is_open());
// Chain B: Confirm
manager_b
.chan_open_confirm(&port, &chan_b, vec![1, 2, 3], Height::from_height(102))
.unwrap();
assert!(manager_b.get_channel(&port, &chan_b).unwrap().is_open());
}
#[test]
fn test_sequence_management() {
let mut manager = ChannelManager::new();
let port = PortId::transfer();
let conn = ConnectionId::new(0);
manager.bind_port(port.clone(), "transfer".to_string()).unwrap();
let channel = manager
.chan_open_init(
port.clone(),
ChannelOrder::Ordered,
vec![conn],
port.clone(),
"ics20-1".to_string(),
)
.unwrap();
// Initial sequences should be 1
let seq = manager.get_sequences(&port, &channel);
assert_eq!(seq.next_send, 1);
assert_eq!(seq.next_recv, 1);
// Increment send
let send_seq = manager.increment_send_sequence(&port, &channel);
assert_eq!(send_seq, 1);
let seq = manager.get_sequences(&port, &channel);
assert_eq!(seq.next_send, 2);
}
}