synor/crates/synor-ibc/src/packet.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

566 lines
16 KiB
Rust

//! IBC Packet Handling
//!
//! Packets are the fundamental unit of data transfer in IBC.
//! Each packet contains application data and routing information.
use crate::channel::{ChannelId, PortId};
use crate::error::{IbcError, IbcResult};
use crate::types::{Height, Timestamp};
use crate::MAX_PACKET_DATA_SIZE;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
/// IBC Packet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Packet {
/// Packet sequence number
pub sequence: u64,
/// Source port
pub source_port: PortId,
/// Source channel
pub source_channel: ChannelId,
/// Destination port
pub dest_port: PortId,
/// Destination channel
pub dest_channel: ChannelId,
/// Packet data (app-specific)
pub data: Vec<u8>,
/// Timeout height (0 = disabled)
pub timeout_height: Height,
/// Timeout timestamp in nanoseconds (0 = disabled)
pub timeout_timestamp: Timestamp,
}
impl Packet {
/// Create a new packet
pub fn new(
sequence: u64,
source_port: PortId,
source_channel: ChannelId,
dest_port: PortId,
dest_channel: ChannelId,
data: Vec<u8>,
timeout_height: Height,
timeout_timestamp: Timestamp,
) -> Self {
Self {
sequence,
source_port,
source_channel,
dest_port,
dest_channel,
data,
timeout_height,
timeout_timestamp,
}
}
/// Check if packet has timed out based on height
pub fn is_timed_out_by_height(&self, current_height: Height) -> bool {
!self.timeout_height.is_zero() && current_height >= self.timeout_height
}
/// Check if packet has timed out based on timestamp
pub fn is_timed_out_by_timestamp(&self, current_time: Timestamp) -> bool {
!self.timeout_timestamp.is_zero() && current_time >= self.timeout_timestamp
}
/// Check if packet has timed out
pub fn is_timed_out(&self, current_height: Height, current_time: Timestamp) -> bool {
self.is_timed_out_by_height(current_height) || self.is_timed_out_by_timestamp(current_time)
}
/// Validate the packet
pub fn validate(&self) -> IbcResult<()> {
if self.sequence == 0 {
return Err(IbcError::InvalidPacketSequence {
expected: 1,
actual: 0,
});
}
self.source_port.validate()?;
self.dest_port.validate()?;
if self.data.len() > MAX_PACKET_DATA_SIZE {
return Err(IbcError::PacketDataTooLarge {
size: self.data.len(),
max: MAX_PACKET_DATA_SIZE,
});
}
// At least one timeout must be set
if self.timeout_height.is_zero() && self.timeout_timestamp.is_zero() {
return Err(IbcError::InvalidCommitment(
"at least one timeout must be set".to_string(),
));
}
Ok(())
}
/// Compute packet commitment hash
pub fn commitment(&self) -> PacketCommitment {
let mut hasher = Sha256::new();
hasher.update(&self.timeout_timestamp.nanoseconds().to_be_bytes());
hasher.update(&self.timeout_height.revision_number.to_be_bytes());
hasher.update(&self.timeout_height.revision_height.to_be_bytes());
// Hash the data
let data_hash = Sha256::digest(&self.data);
hasher.update(&data_hash);
PacketCommitment(hasher.finalize().to_vec())
}
}
/// Packet commitment hash
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PacketCommitment(pub Vec<u8>);
impl PacketCommitment {
/// Create from bytes
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
/// Get as bytes
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
/// Get as hex string
pub fn to_hex(&self) -> String {
hex::encode(&self.0)
}
}
/// Packet acknowledgement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Acknowledgement {
/// Success result with optional data
Success(Vec<u8>),
/// Error result
Error(String),
}
impl Acknowledgement {
/// Create a success acknowledgement
pub fn success(data: Vec<u8>) -> Self {
Acknowledgement::Success(data)
}
/// Create an error acknowledgement
pub fn error(msg: impl Into<String>) -> Self {
Acknowledgement::Error(msg.into())
}
/// Check if acknowledgement is success
pub fn is_success(&self) -> bool {
matches!(self, Acknowledgement::Success(_))
}
/// Compute acknowledgement commitment
pub fn commitment(&self) -> PacketCommitment {
let bytes = match self {
Acknowledgement::Success(data) => {
let mut result = vec![0x01]; // Success prefix
result.extend(data);
result
}
Acknowledgement::Error(msg) => {
let mut result = vec![0x00]; // Error prefix
result.extend(msg.as_bytes());
result
}
};
let hash = Sha256::digest(&bytes);
PacketCommitment(hash.to_vec())
}
/// Encode to bytes
pub fn encode(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap_or_default()
}
/// Decode from bytes
pub fn decode(bytes: &[u8]) -> IbcResult<Self> {
serde_json::from_slice(bytes)
.map_err(|e| IbcError::InvalidAcknowledgement(e.to_string()))
}
}
/// Timeout information
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Timeout {
/// Timeout height
pub height: Height,
/// Timeout timestamp
pub timestamp: Timestamp,
}
impl Timeout {
/// Create a new timeout
pub fn new(height: Height, timestamp: Timestamp) -> Self {
Self { height, timestamp }
}
/// Create a height-only timeout
pub fn height(height: u64) -> Self {
Self {
height: Height::from_height(height),
timestamp: Timestamp::default(),
}
}
/// Create a timestamp-only timeout
pub fn timestamp(timestamp: u64) -> Self {
Self {
height: Height::default(),
timestamp: Timestamp::from_nanoseconds(timestamp),
}
}
/// Check if timed out
pub fn is_timed_out(&self, current_height: Height, current_time: Timestamp) -> bool {
(!self.height.is_zero() && current_height >= self.height)
|| (!self.timestamp.is_zero() && current_time >= self.timestamp)
}
}
impl Default for Timeout {
fn default() -> Self {
Self {
height: Height::default(),
timestamp: Timestamp::default(),
}
}
}
/// Packet receipt (for unordered channels)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PacketReceipt {
/// Received at height
pub received_at: Height,
/// Acknowledgement (if processed)
pub acknowledgement: Option<Acknowledgement>,
}
/// Packet state tracking
pub struct PacketHandler {
/// Sent packet commitments by (port, channel, sequence)
sent_commitments: HashMap<(PortId, ChannelId, u64), PacketCommitment>,
/// Received packet receipts by (port, channel, sequence)
receipts: HashMap<(PortId, ChannelId, u64), PacketReceipt>,
/// Acknowledgement commitments by (port, channel, sequence)
ack_commitments: HashMap<(PortId, ChannelId, u64), PacketCommitment>,
}
impl PacketHandler {
/// Create a new packet handler
pub fn new() -> Self {
Self {
sent_commitments: HashMap::new(),
receipts: HashMap::new(),
ack_commitments: HashMap::new(),
}
}
/// Store a sent packet commitment
pub fn store_packet_commitment(&mut self, packet: &Packet) {
let key = (
packet.source_port.clone(),
packet.source_channel.clone(),
packet.sequence,
);
self.sent_commitments.insert(key, packet.commitment());
}
/// Get packet commitment
pub fn get_packet_commitment(
&self,
port: &PortId,
channel: &ChannelId,
sequence: u64,
) -> Option<&PacketCommitment> {
self.sent_commitments
.get(&(port.clone(), channel.clone(), sequence))
}
/// Delete packet commitment (after ack received)
pub fn delete_packet_commitment(&mut self, port: &PortId, channel: &ChannelId, sequence: u64) {
self.sent_commitments
.remove(&(port.clone(), channel.clone(), sequence));
}
/// Store packet receipt
pub fn store_packet_receipt(
&mut self,
packet: &Packet,
current_height: Height,
ack: Option<Acknowledgement>,
) {
let key = (
packet.dest_port.clone(),
packet.dest_channel.clone(),
packet.sequence,
);
self.receipts.insert(
key,
PacketReceipt {
received_at: current_height,
acknowledgement: ack,
},
);
}
/// Check if packet was received
pub fn has_packet_receipt(
&self,
port: &PortId,
channel: &ChannelId,
sequence: u64,
) -> bool {
self.receipts
.contains_key(&(port.clone(), channel.clone(), sequence))
}
/// Store acknowledgement commitment
pub fn store_ack_commitment(
&mut self,
port: &PortId,
channel: &ChannelId,
sequence: u64,
ack: &Acknowledgement,
) {
let key = (port.clone(), channel.clone(), sequence);
self.ack_commitments.insert(key, ack.commitment());
}
/// Get acknowledgement commitment
pub fn get_ack_commitment(
&self,
port: &PortId,
channel: &ChannelId,
sequence: u64,
) -> Option<&PacketCommitment> {
self.ack_commitments
.get(&(port.clone(), channel.clone(), sequence))
}
/// Delete acknowledgement commitment (after processed)
pub fn delete_ack_commitment(&mut self, port: &PortId, channel: &ChannelId, sequence: u64) {
self.ack_commitments
.remove(&(port.clone(), channel.clone(), sequence));
}
/// Verify packet hasn't already been received (for unordered channels)
pub fn verify_no_receipt(&self, packet: &Packet) -> IbcResult<()> {
if self.has_packet_receipt(&packet.dest_port, &packet.dest_channel, packet.sequence) {
return Err(IbcError::PacketAlreadyReceived(packet.sequence));
}
Ok(())
}
/// Verify packet sequence for ordered channels
pub fn verify_ordered_sequence(
&self,
packet: &Packet,
expected_sequence: u64,
) -> IbcResult<()> {
if packet.sequence != expected_sequence {
return Err(IbcError::InvalidPacketSequence {
expected: expected_sequence,
actual: packet.sequence,
});
}
Ok(())
}
/// Process timeout for a packet
pub fn timeout_packet(&mut self, packet: &Packet) -> IbcResult<()> {
// Verify commitment exists
let key = (
packet.source_port.clone(),
packet.source_channel.clone(),
packet.sequence,
);
if !self.sent_commitments.contains_key(&key) {
return Err(IbcError::PacketNotFound(packet.sequence));
}
// Remove commitment
self.sent_commitments.remove(&key);
Ok(())
}
}
impl Default for PacketHandler {
fn default() -> Self {
Self::new()
}
}
/// Transfer packet data (ICS-20)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FungibleTokenPacketData {
/// Token denomination
pub denom: String,
/// Amount to transfer
pub amount: String,
/// Sender address
pub sender: String,
/// Receiver address
pub receiver: String,
/// Optional memo
pub memo: String,
}
impl FungibleTokenPacketData {
/// Create a new transfer packet
pub fn new(
denom: impl Into<String>,
amount: impl Into<String>,
sender: impl Into<String>,
receiver: impl Into<String>,
) -> Self {
Self {
denom: denom.into(),
amount: amount.into(),
sender: sender.into(),
receiver: receiver.into(),
memo: String::new(),
}
}
/// Add memo
pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
self.memo = memo.into();
self
}
/// Encode to bytes
pub fn encode(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap_or_default()
}
/// Decode from bytes
pub fn decode(bytes: &[u8]) -> IbcResult<Self> {
serde_json::from_slice(bytes)
.map_err(|e| IbcError::DeserializationError(e.to_string()))
}
/// Get the denomination trace path
pub fn get_denom_trace(&self) -> Vec<String> {
self.denom.split('/').map(String::from).collect()
}
/// Check if this is a native token
pub fn is_native(&self) -> bool {
!self.denom.contains('/')
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_packet() -> Packet {
Packet::new(
1,
PortId::transfer(),
ChannelId::new(0),
PortId::transfer(),
ChannelId::new(0),
b"test data".to_vec(),
Height::from_height(100),
Timestamp::default(),
)
}
#[test]
fn test_packet_commitment() {
let packet = test_packet();
let commitment = packet.commitment();
assert!(!commitment.as_bytes().is_empty());
// Same packet should have same commitment
let commitment2 = packet.commitment();
assert_eq!(commitment, commitment2);
}
#[test]
fn test_packet_timeout() {
let packet = test_packet();
// Not timed out at lower height
assert!(!packet.is_timed_out_by_height(Height::from_height(50)));
// Timed out at same height
assert!(packet.is_timed_out_by_height(Height::from_height(100)));
// Timed out at higher height
assert!(packet.is_timed_out_by_height(Height::from_height(150)));
}
#[test]
fn test_acknowledgement() {
let ack = Acknowledgement::success(b"ok".to_vec());
assert!(ack.is_success());
let ack_err = Acknowledgement::error("failed");
assert!(!ack_err.is_success());
// Commitments should be different
assert_ne!(ack.commitment(), ack_err.commitment());
}
#[test]
fn test_packet_handler() {
let mut handler = PacketHandler::new();
let packet = test_packet();
// Store commitment
handler.store_packet_commitment(&packet);
assert!(handler
.get_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence)
.is_some());
// Store receipt
handler.store_packet_receipt(&packet, Height::from_height(50), None);
assert!(handler.has_packet_receipt(
&packet.dest_port,
&packet.dest_channel,
packet.sequence
));
// Verify no duplicate receipt
assert!(handler.verify_no_receipt(&packet).is_err());
}
#[test]
fn test_transfer_packet_data() {
let data = FungibleTokenPacketData::new("uatom", "1000000", "cosmos1...", "synor1...");
let encoded = data.encode();
let decoded = FungibleTokenPacketData::decode(&encoded).unwrap();
assert_eq!(decoded.denom, data.denom);
assert_eq!(decoded.amount, data.amount);
}
#[test]
fn test_denom_trace() {
// Native token
let native = FungibleTokenPacketData::new("usynor", "1000", "a", "b");
assert!(native.is_native());
// IBC token
let ibc = FungibleTokenPacketData::new("transfer/channel-0/uatom", "1000", "a", "b");
assert!(!ibc.is_native());
assert_eq!(ibc.get_denom_trace(), vec!["transfer", "channel-0", "uatom"]);
}
}