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
566 lines
16 KiB
Rust
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"]);
|
|
}
|
|
}
|