//! 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, /// 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, 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); impl PacketCommitment { /// Create from bytes pub fn new(bytes: Vec) -> 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), /// Error result Error(String), } impl Acknowledgement { /// Create a success acknowledgement pub fn success(data: Vec) -> Self { Acknowledgement::Success(data) } /// Create an error acknowledgement pub fn error(msg: impl Into) -> 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 { serde_json::to_vec(self).unwrap_or_default() } /// Decode from bytes pub fn decode(bytes: &[u8]) -> IbcResult { 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, } /// 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, ) { 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, amount: impl Into, sender: impl Into, receiver: impl Into, ) -> 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) -> Self { self.memo = memo.into(); self } /// Encode to bytes pub fn encode(&self) -> Vec { serde_json::to_vec(self).unwrap_or_default() } /// Decode from bytes pub fn decode(bytes: &[u8]) -> IbcResult { 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 { 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"]); } }