use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use crate::error::{CoreError, Result};
pub use super::atomic_swaps::Chain;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelState {
Init,
TryOpen,
Open,
Closing,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IBCChannel {
pub id: String,
pub source_chain: Chain,
pub destination_chain: Chain,
pub state: ChannelState,
pub connection_id: String,
pub port_id: String,
pub counterparty_channel_id: String,
pub version: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl IBCChannel {
pub fn new(
id: String,
source_chain: Chain,
destination_chain: Chain,
connection_id: String,
port_id: String,
version: String,
) -> Self {
let now = Utc::now();
Self {
id,
source_chain,
destination_chain,
state: ChannelState::Init,
connection_id,
port_id,
counterparty_channel_id: String::new(),
version,
created_at: now,
updated_at: now,
}
}
pub fn open(&mut self, counterparty_channel_id: String) -> Result<()> {
if self.state != ChannelState::TryOpen {
return Err(CoreError::Validation(format!(
"Cannot open channel in state {:?}",
self.state
)));
}
self.state = ChannelState::Open;
self.counterparty_channel_id = counterparty_channel_id;
self.updated_at = Utc::now();
Ok(())
}
pub fn close(&mut self) -> Result<()> {
if self.state != ChannelState::Open {
return Err(CoreError::Validation(format!(
"Cannot close channel in state {:?}",
self.state
)));
}
self.state = ChannelState::Closing;
self.updated_at = Utc::now();
Ok(())
}
pub fn is_open(&self) -> bool {
self.state == ChannelState::Open
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IBCPacket {
pub sequence: u64,
pub source_channel: String,
pub destination_channel: String,
pub data: Vec<u8>,
pub timeout_height: Option<u64>,
pub timeout_timestamp: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
impl IBCPacket {
pub fn new(
sequence: u64,
source_channel: String,
destination_channel: String,
data: Vec<u8>,
timeout_height: Option<u64>,
timeout_timestamp: Option<DateTime<Utc>>,
) -> Self {
Self {
sequence,
source_channel,
destination_channel,
data,
timeout_height,
timeout_timestamp,
created_at: Utc::now(),
}
}
pub fn is_timed_out(&self, current_height: u64, current_time: DateTime<Utc>) -> bool {
if let Some(timeout_height) = self.timeout_height {
if current_height >= timeout_height {
return true;
}
}
if let Some(timeout_timestamp) = self.timeout_timestamp {
if current_time >= timeout_timestamp {
return true;
}
}
false
}
pub fn hash(&self) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(self.sequence.to_le_bytes());
hasher.update(self.source_channel.as_bytes());
hasher.update(self.destination_channel.as_bytes());
hasher.update(&self.data);
if let Some(height) = self.timeout_height {
hasher.update(height.to_le_bytes());
}
if let Some(timestamp) = self.timeout_timestamp {
hasher.update(timestamp.timestamp().to_le_bytes());
}
hasher.finalize().to_vec()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossChainMessage {
pub id: String,
pub source_chain: Chain,
pub destination_chain: Chain,
pub sender: String,
pub receiver: String,
pub message_type: String,
pub payload: Vec<u8>,
pub nonce: u64,
pub gas_limit: u64,
pub status: MessageStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageStatus {
Pending,
Relaying,
Executing,
Delivered,
Failed,
TimedOut,
}
impl CrossChainMessage {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
source_chain: Chain,
destination_chain: Chain,
sender: String,
receiver: String,
message_type: String,
payload: Vec<u8>,
nonce: u64,
gas_limit: u64,
) -> Self {
let now = Utc::now();
Self {
id,
source_chain,
destination_chain,
sender,
receiver,
message_type,
payload,
nonce,
gas_limit,
status: MessageStatus::Pending,
created_at: now,
updated_at: now,
}
}
pub fn mark_relaying(&mut self) {
self.status = MessageStatus::Relaying;
self.updated_at = Utc::now();
}
pub fn mark_executing(&mut self) {
self.status = MessageStatus::Executing;
self.updated_at = Utc::now();
}
pub fn mark_delivered(&mut self) {
self.status = MessageStatus::Delivered;
self.updated_at = Utc::now();
}
pub fn mark_failed(&mut self) {
self.status = MessageStatus::Failed;
self.updated_at = Utc::now();
}
pub fn hash(&self) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(self.id.as_bytes());
hasher.update(self.nonce.to_le_bytes());
hasher.update(&self.payload);
hasher.finalize().to_vec()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateProof {
pub height: u64,
pub state_root: Vec<u8>,
pub proof: Vec<Vec<u8>>,
pub value: Vec<u8>,
pub timestamp: DateTime<Utc>,
}
impl StateProof {
pub fn new(height: u64, state_root: Vec<u8>, proof: Vec<Vec<u8>>, value: Vec<u8>) -> Self {
Self {
height,
state_root,
proof,
value,
timestamp: Utc::now(),
}
}
pub fn verify(&self, key: &[u8]) -> Result<bool> {
let mut current_hash = self.compute_leaf_hash(key, &self.value);
for sibling in &self.proof {
current_hash = self.compute_internal_hash(¤t_hash, sibling);
}
Ok(current_hash == self.state_root)
}
fn compute_leaf_hash(&self, key: &[u8], value: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(b"leaf:");
hasher.update(key);
hasher.update(value);
hasher.finalize().to_vec()
}
fn compute_internal_hash(&self, left: &[u8], right: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(b"node:");
if left <= right {
hasher.update(left);
hasher.update(right);
} else {
hasher.update(right);
hasher.update(left);
}
hasher.finalize().to_vec()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightClientHeader {
pub chain: Chain,
pub height: u64,
pub block_hash: Vec<u8>,
pub parent_hash: Vec<u8>,
pub state_root: Vec<u8>,
pub timestamp: DateTime<Utc>,
pub validator_set_hash: Vec<u8>,
pub signatures: Vec<ValidatorSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorSignature {
pub validator: String,
pub signature: Vec<u8>,
pub voting_power: u64,
}
impl LightClientHeader {
pub fn new(
chain: Chain,
height: u64,
block_hash: Vec<u8>,
parent_hash: Vec<u8>,
state_root: Vec<u8>,
timestamp: DateTime<Utc>,
validator_set_hash: Vec<u8>,
) -> Self {
Self {
chain,
height,
block_hash,
parent_hash,
state_root,
timestamp,
validator_set_hash,
signatures: Vec::new(),
}
}
pub fn add_signature(&mut self, signature: ValidatorSignature) {
self.signatures.push(signature);
}
pub fn total_voting_power(&self) -> u64 {
self.signatures.iter().map(|s| s.voting_power).sum()
}
pub fn verify_threshold(
&self,
total_validator_power: u64,
threshold_numerator: u64,
threshold_denominator: u64,
) -> bool {
let signed_power = self.total_voting_power();
signed_power * threshold_denominator >= total_validator_power * threshold_numerator
}
pub fn hash(&self) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(self.height.to_le_bytes());
hasher.update(&self.block_hash);
hasher.update(&self.parent_hash);
hasher.update(&self.state_root);
hasher.update(self.timestamp.timestamp().to_le_bytes());
hasher.update(&self.validator_set_hash);
hasher.finalize().to_vec()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightClient {
pub chain: Chain,
pub latest_height: u64,
pub trusted_headers: HashMap<u64, LightClientHeader>,
pub max_header_age_seconds: i64,
pub trusting_period_seconds: i64,
}
impl LightClient {
pub fn new(chain: Chain, trusting_period_seconds: i64) -> Self {
Self {
chain,
latest_height: 0,
trusted_headers: HashMap::new(),
max_header_age_seconds: trusting_period_seconds,
trusting_period_seconds,
}
}
pub fn update(&mut self, header: LightClientHeader) -> Result<()> {
if header.chain != self.chain {
return Err(CoreError::Validation(format!(
"Header is for chain {:?}, expected {:?}",
header.chain, self.chain
)));
}
if header.height <= self.latest_height {
return Err(CoreError::Validation(format!(
"Header height {} is not newer than latest height {}",
header.height, self.latest_height
)));
}
let now = Utc::now();
let age_seconds = (now - header.timestamp).num_seconds();
if age_seconds > self.max_header_age_seconds {
return Err(CoreError::Validation(format!(
"Header is too old: {} seconds > {} max",
age_seconds, self.max_header_age_seconds
)));
}
if let Some(parent) = self.trusted_headers.get(&(header.height - 1)) {
if parent.block_hash != header.parent_hash {
return Err(CoreError::Validation("Parent hash mismatch".to_string()));
}
}
self.latest_height = header.height;
self.trusted_headers.insert(header.height, header);
self.prune_old_headers(now);
Ok(())
}
pub fn get_header(&self, height: u64) -> Option<&LightClientHeader> {
self.trusted_headers.get(&height)
}
pub fn verify_state(&self, height: u64, proof: &StateProof, key: &[u8]) -> Result<bool> {
let header = self.get_header(height).ok_or_else(|| {
CoreError::Validation(format!("No trusted header at height {}", height))
})?;
if proof.height != height {
return Err(CoreError::Validation(format!(
"Proof height {} doesn't match requested height {}",
proof.height, height
)));
}
if proof.state_root != header.state_root {
return Err(CoreError::Validation("State root mismatch".to_string()));
}
proof.verify(key)
}
fn prune_old_headers(&mut self, now: DateTime<Utc>) {
let cutoff_timestamp = now - chrono::Duration::seconds(self.trusting_period_seconds);
self.trusted_headers
.retain(|_, header| header.timestamp >= cutoff_timestamp);
}
}
#[derive(Debug)]
pub struct IBCConnectionManager {
pub channels: HashMap<String, IBCChannel>,
pub packet_sequences: HashMap<String, u64>,
pub pending_packets: Vec<IBCPacket>,
}
impl IBCConnectionManager {
pub fn new() -> Self {
Self {
channels: HashMap::new(),
packet_sequences: HashMap::new(),
pending_packets: Vec::new(),
}
}
pub fn register_channel(&mut self, channel: IBCChannel) -> Result<()> {
if self.channels.contains_key(&channel.id) {
return Err(CoreError::Validation(format!(
"Channel {} already exists",
channel.id
)));
}
self.packet_sequences.insert(channel.id.clone(), 0);
self.channels.insert(channel.id.clone(), channel);
Ok(())
}
pub fn try_open_channel(&mut self, channel_id: &str) -> Result<()> {
let channel = self
.channels
.get_mut(channel_id)
.ok_or_else(|| CoreError::Validation(format!("Channel {} not found", channel_id)))?;
if channel.state != ChannelState::Init {
return Err(CoreError::Validation(format!(
"Cannot try_open channel in state {:?}",
channel.state
)));
}
channel.state = ChannelState::TryOpen;
channel.updated_at = Utc::now();
Ok(())
}
pub fn open_channel(
&mut self,
channel_id: &str,
counterparty_channel_id: String,
) -> Result<()> {
let channel = self
.channels
.get_mut(channel_id)
.ok_or_else(|| CoreError::Validation(format!("Channel {} not found", channel_id)))?;
channel.open(counterparty_channel_id)?;
Ok(())
}
pub fn send_packet(
&mut self,
channel_id: &str,
data: Vec<u8>,
timeout_height: Option<u64>,
timeout_timestamp: Option<DateTime<Utc>>,
) -> Result<IBCPacket> {
let channel = self
.channels
.get(channel_id)
.ok_or_else(|| CoreError::Validation(format!("Channel {} not found", channel_id)))?;
if !channel.is_open() {
return Err(CoreError::Validation(format!(
"Channel {} is not open",
channel_id
)));
}
let sequence = self.packet_sequences.get_mut(channel_id).unwrap();
*sequence += 1;
let packet = IBCPacket::new(
*sequence,
channel.id.clone(),
channel.counterparty_channel_id.clone(),
data,
timeout_height,
timeout_timestamp,
);
self.pending_packets.push(packet.clone());
Ok(packet)
}
pub fn receive_packet(
&mut self,
packet: &IBCPacket,
current_height: u64,
current_time: DateTime<Utc>,
) -> Result<()> {
if packet.is_timed_out(current_height, current_time) {
return Err(CoreError::Validation("Packet has timed out".to_string()));
}
self.pending_packets
.retain(|p| p.sequence != packet.sequence || p.source_channel != packet.source_channel);
Ok(())
}
pub fn get_channel(&self, channel_id: &str) -> Option<&IBCChannel> {
self.channels.get(channel_id)
}
pub fn get_channels_between(&self, source: Chain, destination: Chain) -> Vec<&IBCChannel> {
self.channels
.values()
.filter(|c| c.source_chain == source && c.destination_chain == destination)
.collect()
}
}
impl Default for IBCConnectionManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ibc_channel_lifecycle() {
let mut channel = IBCChannel::new(
"channel-0".to_string(),
Chain::Bitcoin,
Chain::Ethereum,
"connection-0".to_string(),
"transfer".to_string(),
"ics20-1".to_string(),
);
assert_eq!(channel.state, ChannelState::Init);
assert!(!channel.is_open());
channel.state = ChannelState::TryOpen;
channel.open("channel-1".to_string()).unwrap();
assert!(channel.is_open());
channel.close().unwrap();
assert_eq!(channel.state, ChannelState::Closing);
}
#[test]
fn test_ibc_packet_timeout() {
let timeout_height = Some(100);
let timeout_timestamp = Some(Utc::now() + chrono::Duration::seconds(3600));
let packet = IBCPacket::new(
1,
"channel-0".to_string(),
"channel-1".to_string(),
vec![1, 2, 3],
timeout_height,
timeout_timestamp,
);
assert!(!packet.is_timed_out(50, Utc::now()));
assert!(packet.is_timed_out(100, Utc::now()));
assert!(packet.is_timed_out(50, Utc::now() + chrono::Duration::seconds(7200)));
}
#[test]
fn test_cross_chain_message() {
let mut message = CrossChainMessage::new(
"msg-1".to_string(),
Chain::Bitcoin,
Chain::Ethereum,
"sender".to_string(),
"receiver".to_string(),
"token_transfer".to_string(),
vec![1, 2, 3],
1,
100000,
);
assert_eq!(message.status, MessageStatus::Pending);
message.mark_relaying();
assert_eq!(message.status, MessageStatus::Relaying);
message.mark_executing();
assert_eq!(message.status, MessageStatus::Executing);
message.mark_delivered();
assert_eq!(message.status, MessageStatus::Delivered);
}
#[test]
fn test_light_client() {
let mut client = LightClient::new(Chain::Bitcoin, 86400);
let header = LightClientHeader::new(
Chain::Bitcoin,
100,
vec![1, 2, 3],
vec![0, 1, 2],
vec![3, 4, 5],
Utc::now(),
vec![6, 7, 8],
);
client.update(header.clone()).unwrap();
assert_eq!(client.latest_height, 100);
let retrieved = client.get_header(100).unwrap();
assert_eq!(retrieved.height, 100);
}
#[test]
fn test_ibc_connection_manager() {
let mut manager = IBCConnectionManager::new();
let channel = IBCChannel::new(
"channel-0".to_string(),
Chain::Bitcoin,
Chain::Ethereum,
"connection-0".to_string(),
"transfer".to_string(),
"ics20-1".to_string(),
);
manager.register_channel(channel).unwrap();
assert!(manager.get_channel("channel-0").is_some());
manager.try_open_channel("channel-0").unwrap();
manager
.open_channel("channel-0", "channel-1".to_string())
.unwrap();
let packet = manager
.send_packet("channel-0", vec![1, 2, 3], Some(1000), None)
.unwrap();
assert_eq!(packet.sequence, 1);
assert_eq!(manager.pending_packets.len(), 1);
}
}