685 lines
21 KiB
Rust
685 lines
21 KiB
Rust
//! IBC Handler - Main interface for IBC operations
|
|
//!
|
|
//! The handler coordinates light clients, connections, channels, and packets.
|
|
|
|
use crate::channel::{Channel, ChannelId, ChannelManager, ChannelOrder, PortId};
|
|
use crate::client::{ClientId, ClientState, ConsensusState, Header, LightClient};
|
|
use crate::connection::{ConnectionEnd, ConnectionId, ConnectionManager, Counterparty};
|
|
use crate::error::{IbcError, IbcResult};
|
|
use crate::packet::{Acknowledgement, FungibleTokenPacketData, Packet, PacketHandler, Timeout};
|
|
use crate::types::{Height, Timestamp, Version};
|
|
use parking_lot::RwLock;
|
|
use std::sync::Arc;
|
|
|
|
/// IBC handler configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct IbcConfig {
|
|
/// Chain ID
|
|
pub chain_id: String,
|
|
/// Default connection delay period (nanoseconds)
|
|
pub default_delay_period: u64,
|
|
/// Enable packet timeout
|
|
pub enable_timeout: bool,
|
|
/// Maximum packet data size
|
|
pub max_packet_size: usize,
|
|
}
|
|
|
|
impl Default for IbcConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
chain_id: "synor-1".to_string(),
|
|
default_delay_period: 0,
|
|
enable_timeout: true,
|
|
max_packet_size: 1024 * 1024, // 1 MB
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Main IBC handler
|
|
pub struct IbcHandler {
|
|
/// Configuration
|
|
config: IbcConfig,
|
|
/// Light client manager
|
|
light_client: Arc<RwLock<LightClient>>,
|
|
/// Connection manager
|
|
connections: Arc<RwLock<ConnectionManager>>,
|
|
/// Channel manager
|
|
channels: Arc<RwLock<ChannelManager>>,
|
|
/// Packet handler
|
|
packets: Arc<RwLock<PacketHandler>>,
|
|
/// Current block height
|
|
current_height: Arc<RwLock<Height>>,
|
|
/// Current block timestamp
|
|
current_timestamp: Arc<RwLock<Timestamp>>,
|
|
}
|
|
|
|
impl IbcHandler {
|
|
/// Create a new IBC handler
|
|
pub fn new(config: IbcConfig) -> Self {
|
|
Self {
|
|
config,
|
|
light_client: Arc::new(RwLock::new(LightClient::new())),
|
|
connections: Arc::new(RwLock::new(ConnectionManager::new())),
|
|
channels: Arc::new(RwLock::new(ChannelManager::new())),
|
|
packets: Arc::new(RwLock::new(PacketHandler::new())),
|
|
current_height: Arc::new(RwLock::new(Height::from_height(1))),
|
|
current_timestamp: Arc::new(RwLock::new(Timestamp::now())),
|
|
}
|
|
}
|
|
|
|
/// Update current block height and timestamp
|
|
pub fn update_block(&self, height: Height, timestamp: Timestamp) {
|
|
*self.current_height.write() = height;
|
|
*self.current_timestamp.write() = timestamp;
|
|
}
|
|
|
|
/// Get current height
|
|
pub fn current_height(&self) -> Height {
|
|
*self.current_height.read()
|
|
}
|
|
|
|
/// Get current timestamp
|
|
pub fn current_timestamp(&self) -> Timestamp {
|
|
*self.current_timestamp.read()
|
|
}
|
|
|
|
// ========================================================================
|
|
// Client Operations
|
|
// ========================================================================
|
|
|
|
/// Create a new light client
|
|
pub fn create_client(
|
|
&self,
|
|
client_state: ClientState,
|
|
consensus_state: ConsensusState,
|
|
) -> IbcResult<ClientId> {
|
|
let mut client = self.light_client.write();
|
|
let client_id = client.create_client(client_state, consensus_state)?;
|
|
|
|
tracing::info!(
|
|
client_id = %client_id,
|
|
"Created IBC client"
|
|
);
|
|
|
|
Ok(client_id)
|
|
}
|
|
|
|
/// Update a light client with a new header
|
|
pub fn update_client(&self, client_id: &ClientId, header: Header) -> IbcResult<()> {
|
|
let mut client = self.light_client.write();
|
|
client.update_client(client_id, header)?;
|
|
|
|
tracing::info!(
|
|
client_id = %client_id,
|
|
"Updated IBC client"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get client state
|
|
pub fn get_client_state(&self, client_id: &ClientId) -> IbcResult<ClientState> {
|
|
let client = self.light_client.read();
|
|
Ok(client.get_client_state(client_id)?.clone())
|
|
}
|
|
|
|
// ========================================================================
|
|
// Connection Operations
|
|
// ========================================================================
|
|
|
|
/// Initialize a connection (ConnOpenInit)
|
|
pub fn connection_open_init(
|
|
&self,
|
|
client_id: ClientId,
|
|
counterparty_client_id: ClientId,
|
|
version: Option<Version>,
|
|
) -> IbcResult<ConnectionId> {
|
|
// Verify client exists
|
|
let _ = self.get_client_state(&client_id)?;
|
|
|
|
let counterparty = Counterparty::new(
|
|
counterparty_client_id,
|
|
None,
|
|
crate::types::CommitmentPrefix::default(),
|
|
);
|
|
|
|
let mut connections = self.connections.write();
|
|
connections.conn_open_init(
|
|
client_id,
|
|
counterparty,
|
|
version,
|
|
self.config.default_delay_period,
|
|
)
|
|
}
|
|
|
|
/// Try to open a connection (ConnOpenTry)
|
|
pub fn connection_open_try(
|
|
&self,
|
|
client_id: ClientId,
|
|
counterparty_client_id: ClientId,
|
|
counterparty_conn_id: ConnectionId,
|
|
counterparty_versions: Vec<Version>,
|
|
proof_init: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<ConnectionId> {
|
|
// Verify client exists
|
|
let _ = self.get_client_state(&client_id)?;
|
|
|
|
let counterparty = Counterparty::new(
|
|
counterparty_client_id,
|
|
Some(counterparty_conn_id),
|
|
crate::types::CommitmentPrefix::default(),
|
|
);
|
|
|
|
let mut connections = self.connections.write();
|
|
connections.conn_open_try(
|
|
client_id,
|
|
counterparty,
|
|
counterparty_versions,
|
|
self.config.default_delay_period,
|
|
proof_init,
|
|
proof_height,
|
|
)
|
|
}
|
|
|
|
/// Acknowledge a connection (ConnOpenAck)
|
|
pub fn connection_open_ack(
|
|
&self,
|
|
conn_id: &ConnectionId,
|
|
counterparty_conn_id: ConnectionId,
|
|
version: Version,
|
|
proof_try: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<()> {
|
|
let mut connections = self.connections.write();
|
|
connections.conn_open_ack(conn_id, counterparty_conn_id, version, proof_try, proof_height)
|
|
}
|
|
|
|
/// Confirm a connection (ConnOpenConfirm)
|
|
pub fn connection_open_confirm(
|
|
&self,
|
|
conn_id: &ConnectionId,
|
|
proof_ack: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<()> {
|
|
let mut connections = self.connections.write();
|
|
connections.conn_open_confirm(conn_id, proof_ack, proof_height)
|
|
}
|
|
|
|
/// Get connection
|
|
pub fn get_connection(&self, conn_id: &ConnectionId) -> IbcResult<ConnectionEnd> {
|
|
let connections = self.connections.read();
|
|
Ok(connections.get_connection(conn_id)?.clone())
|
|
}
|
|
|
|
// ========================================================================
|
|
// Channel Operations
|
|
// ========================================================================
|
|
|
|
/// Bind a port to a module
|
|
pub fn bind_port(&self, port_id: PortId, module: String) -> IbcResult<()> {
|
|
let mut channels = self.channels.write();
|
|
channels.bind_port(port_id, module)
|
|
}
|
|
|
|
/// Initialize a channel (ChanOpenInit)
|
|
pub fn channel_open_init(
|
|
&self,
|
|
port_id: PortId,
|
|
connection_id: ConnectionId,
|
|
counterparty_port: PortId,
|
|
ordering: ChannelOrder,
|
|
version: String,
|
|
) -> IbcResult<ChannelId> {
|
|
// Verify connection exists and is open
|
|
let connection = self.get_connection(&connection_id)?;
|
|
if !connection.is_open() {
|
|
return Err(IbcError::InvalidConnectionState {
|
|
expected: "OPEN".to_string(),
|
|
actual: connection.state.to_string(),
|
|
});
|
|
}
|
|
|
|
let mut channels = self.channels.write();
|
|
channels.chan_open_init(
|
|
port_id,
|
|
ordering,
|
|
vec![connection_id],
|
|
counterparty_port,
|
|
version,
|
|
)
|
|
}
|
|
|
|
/// Try to open a channel (ChanOpenTry)
|
|
pub fn channel_open_try(
|
|
&self,
|
|
port_id: PortId,
|
|
connection_id: ConnectionId,
|
|
counterparty_port: PortId,
|
|
counterparty_channel: ChannelId,
|
|
ordering: ChannelOrder,
|
|
version: String,
|
|
counterparty_version: String,
|
|
proof_init: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<ChannelId> {
|
|
// Verify connection
|
|
let connection = self.get_connection(&connection_id)?;
|
|
if !connection.is_open() {
|
|
return Err(IbcError::InvalidConnectionState {
|
|
expected: "OPEN".to_string(),
|
|
actual: connection.state.to_string(),
|
|
});
|
|
}
|
|
|
|
let mut channels = self.channels.write();
|
|
channels.chan_open_try(
|
|
port_id,
|
|
ordering,
|
|
vec![connection_id],
|
|
counterparty_port,
|
|
counterparty_channel,
|
|
version,
|
|
counterparty_version,
|
|
proof_init,
|
|
proof_height,
|
|
)
|
|
}
|
|
|
|
/// Acknowledge a channel (ChanOpenAck)
|
|
pub fn channel_open_ack(
|
|
&self,
|
|
port_id: &PortId,
|
|
channel_id: &ChannelId,
|
|
counterparty_channel: ChannelId,
|
|
counterparty_version: String,
|
|
proof_try: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<()> {
|
|
let mut channels = self.channels.write();
|
|
channels.chan_open_ack(
|
|
port_id,
|
|
channel_id,
|
|
counterparty_channel,
|
|
counterparty_version,
|
|
proof_try,
|
|
proof_height,
|
|
)
|
|
}
|
|
|
|
/// Confirm a channel (ChanOpenConfirm)
|
|
pub fn channel_open_confirm(
|
|
&self,
|
|
port_id: &PortId,
|
|
channel_id: &ChannelId,
|
|
proof_ack: Vec<u8>,
|
|
proof_height: Height,
|
|
) -> IbcResult<()> {
|
|
let mut channels = self.channels.write();
|
|
channels.chan_open_confirm(port_id, channel_id, proof_ack, proof_height)
|
|
}
|
|
|
|
/// Close a channel
|
|
pub fn channel_close_init(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<()> {
|
|
let mut channels = self.channels.write();
|
|
channels.chan_close_init(port_id, channel_id)
|
|
}
|
|
|
|
/// Get channel
|
|
pub fn get_channel(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<Channel> {
|
|
let channels = self.channels.read();
|
|
Ok(channels.get_channel(port_id, channel_id)?.clone())
|
|
}
|
|
|
|
// ========================================================================
|
|
// Packet Operations
|
|
// ========================================================================
|
|
|
|
/// Send a packet
|
|
pub fn send_packet(
|
|
&self,
|
|
source_port: PortId,
|
|
source_channel: ChannelId,
|
|
data: Vec<u8>,
|
|
timeout: Timeout,
|
|
) -> IbcResult<u64> {
|
|
// Validate data size
|
|
if data.len() > self.config.max_packet_size {
|
|
return Err(IbcError::PacketDataTooLarge {
|
|
size: data.len(),
|
|
max: self.config.max_packet_size,
|
|
});
|
|
}
|
|
|
|
// Get channel and verify it's open
|
|
let channel = self.get_channel(&source_port, &source_channel)?;
|
|
if !channel.is_open() {
|
|
return Err(IbcError::InvalidChannelState {
|
|
expected: "OPEN".to_string(),
|
|
actual: channel.state.to_string(),
|
|
});
|
|
}
|
|
|
|
let counterparty = &channel.counterparty;
|
|
let dest_port = counterparty.port_id.clone();
|
|
let dest_channel = counterparty.channel_id.clone().ok_or_else(|| {
|
|
IbcError::InvalidChannelState {
|
|
expected: "counterparty channel set".to_string(),
|
|
actual: "none".to_string(),
|
|
}
|
|
})?;
|
|
|
|
// Get sequence number
|
|
let mut channels = self.channels.write();
|
|
let sequence = channels.increment_send_sequence(&source_port, &source_channel);
|
|
|
|
// Create packet
|
|
let packet = Packet::new(
|
|
sequence,
|
|
source_port.clone(),
|
|
source_channel.clone(),
|
|
dest_port,
|
|
dest_channel,
|
|
data,
|
|
timeout.height,
|
|
timeout.timestamp,
|
|
);
|
|
packet.validate()?;
|
|
|
|
// Store commitment
|
|
let mut packets = self.packets.write();
|
|
packets.store_packet_commitment(&packet);
|
|
|
|
tracing::info!(
|
|
port = %source_port,
|
|
channel = %source_channel,
|
|
sequence = sequence,
|
|
"Packet sent"
|
|
);
|
|
|
|
Ok(sequence)
|
|
}
|
|
|
|
/// Receive a packet
|
|
pub fn recv_packet(
|
|
&self,
|
|
packet: Packet,
|
|
proof_commitment: Vec<u8>,
|
|
_proof_height: Height,
|
|
) -> IbcResult<Acknowledgement> {
|
|
// Validate packet
|
|
packet.validate()?;
|
|
|
|
// Check timeout
|
|
let current_height = self.current_height();
|
|
let current_time = self.current_timestamp();
|
|
if packet.is_timed_out(current_height, current_time) {
|
|
return Err(IbcError::PacketTimeout {
|
|
height: current_height.revision_height,
|
|
timestamp: current_time.nanoseconds(),
|
|
});
|
|
}
|
|
|
|
// Get channel and verify
|
|
let channel = self.get_channel(&packet.dest_port, &packet.dest_channel)?;
|
|
if !channel.is_open() {
|
|
return Err(IbcError::InvalidChannelState {
|
|
expected: "OPEN".to_string(),
|
|
actual: channel.state.to_string(),
|
|
});
|
|
}
|
|
|
|
// Verify proof (simplified)
|
|
if proof_commitment.is_empty() {
|
|
return Err(IbcError::MissingProof("proof_commitment required".to_string()));
|
|
}
|
|
|
|
// Handle based on channel ordering
|
|
let mut packets = self.packets.write();
|
|
let mut channels = self.channels.write();
|
|
|
|
match channel.ordering {
|
|
ChannelOrder::Unordered => {
|
|
// Verify no receipt exists
|
|
packets.verify_no_receipt(&packet)?;
|
|
}
|
|
ChannelOrder::Ordered => {
|
|
// Verify sequence
|
|
let expected = channels.get_sequences(&packet.dest_port, &packet.dest_channel).next_recv;
|
|
packets.verify_ordered_sequence(&packet, expected)?;
|
|
channels.increment_recv_sequence(&packet.dest_port, &packet.dest_channel);
|
|
}
|
|
}
|
|
|
|
// Store receipt
|
|
packets.store_packet_receipt(&packet, current_height, None);
|
|
|
|
// Generate acknowledgement (success)
|
|
let ack = Acknowledgement::success(vec![]);
|
|
|
|
// Store ack commitment
|
|
packets.store_ack_commitment(
|
|
&packet.dest_port,
|
|
&packet.dest_channel,
|
|
packet.sequence,
|
|
&ack,
|
|
);
|
|
|
|
tracing::info!(
|
|
port = %packet.dest_port,
|
|
channel = %packet.dest_channel,
|
|
sequence = packet.sequence,
|
|
"Packet received"
|
|
);
|
|
|
|
Ok(ack)
|
|
}
|
|
|
|
/// Acknowledge a packet
|
|
pub fn acknowledge_packet(
|
|
&self,
|
|
packet: Packet,
|
|
acknowledgement: Acknowledgement,
|
|
proof_ack: Vec<u8>,
|
|
_proof_height: Height,
|
|
) -> IbcResult<()> {
|
|
// Get channel
|
|
let channel = self.get_channel(&packet.source_port, &packet.source_channel)?;
|
|
|
|
// Verify proof (simplified)
|
|
if proof_ack.is_empty() {
|
|
return Err(IbcError::MissingProof("proof_ack required".to_string()));
|
|
}
|
|
|
|
let mut packets = self.packets.write();
|
|
|
|
// Verify commitment exists
|
|
if packets
|
|
.get_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence)
|
|
.is_none()
|
|
{
|
|
return Err(IbcError::PacketNotFound(packet.sequence));
|
|
}
|
|
|
|
// For ordered channels, verify sequence
|
|
if channel.ordering == ChannelOrder::Ordered {
|
|
let mut channels = self.channels.write();
|
|
let expected = channels.get_sequences(&packet.source_port, &packet.source_channel).next_ack;
|
|
if packet.sequence != expected {
|
|
return Err(IbcError::InvalidPacketSequence {
|
|
expected,
|
|
actual: packet.sequence,
|
|
});
|
|
}
|
|
channels.increment_ack_sequence(&packet.source_port, &packet.source_channel);
|
|
}
|
|
|
|
// Delete commitment
|
|
packets.delete_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence);
|
|
|
|
tracing::info!(
|
|
port = %packet.source_port,
|
|
channel = %packet.source_channel,
|
|
sequence = packet.sequence,
|
|
success = acknowledgement.is_success(),
|
|
"Packet acknowledged"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Timeout a packet
|
|
pub fn timeout_packet(
|
|
&self,
|
|
packet: Packet,
|
|
proof_unreceived: Vec<u8>,
|
|
_proof_height: Height,
|
|
_next_sequence_recv: u64,
|
|
) -> IbcResult<()> {
|
|
// Verify the packet timed out
|
|
let _current_height = self.current_height();
|
|
let _current_time = self.current_timestamp();
|
|
|
|
// Note: For timeout, we check if DESTINATION chain's height/time exceeds timeout
|
|
// This is a simplification - real implementation would verify against proof_height
|
|
|
|
// Verify proof (simplified)
|
|
if proof_unreceived.is_empty() {
|
|
return Err(IbcError::MissingProof("proof_unreceived required".to_string()));
|
|
}
|
|
|
|
let mut packets = self.packets.write();
|
|
packets.timeout_packet(&packet)?;
|
|
|
|
tracing::info!(
|
|
port = %packet.source_port,
|
|
channel = %packet.source_channel,
|
|
sequence = packet.sequence,
|
|
"Packet timed out"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ========================================================================
|
|
// Token Transfer (ICS-20)
|
|
// ========================================================================
|
|
|
|
/// Send tokens via IBC transfer
|
|
pub fn transfer(
|
|
&self,
|
|
source_port: PortId,
|
|
source_channel: ChannelId,
|
|
denom: String,
|
|
amount: String,
|
|
sender: String,
|
|
receiver: String,
|
|
timeout: Timeout,
|
|
) -> IbcResult<u64> {
|
|
let packet_data = FungibleTokenPacketData::new(denom, amount, sender, receiver);
|
|
let data = packet_data.encode();
|
|
|
|
self.send_packet(source_port, source_channel, data, timeout)
|
|
}
|
|
|
|
// ========================================================================
|
|
// Stats and Info
|
|
// ========================================================================
|
|
|
|
/// Get connection count
|
|
pub fn connection_count(&self) -> usize {
|
|
self.connections.read().connection_count()
|
|
}
|
|
|
|
/// Get chain ID
|
|
pub fn chain_id(&self) -> &str {
|
|
&self.config.chain_id
|
|
}
|
|
}
|
|
|
|
impl Default for IbcHandler {
|
|
fn default() -> Self {
|
|
Self::new(IbcConfig::default())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::types::ChainId;
|
|
|
|
fn setup_handler() -> IbcHandler {
|
|
let handler = IbcHandler::new(IbcConfig::default());
|
|
|
|
// Create a client
|
|
let client_state = ClientState::new_synor(
|
|
ChainId::synor_testnet(),
|
|
Height::from_height(100),
|
|
);
|
|
let consensus_state = ConsensusState::new(
|
|
Timestamp::now(),
|
|
vec![0u8; 32],
|
|
vec![0u8; 32],
|
|
);
|
|
|
|
handler.create_client(client_state, consensus_state).unwrap();
|
|
handler.bind_port(PortId::transfer(), "transfer".to_string()).unwrap();
|
|
|
|
handler
|
|
}
|
|
|
|
#[test]
|
|
fn test_handler_creation() {
|
|
let handler = IbcHandler::default();
|
|
assert_eq!(handler.chain_id(), "synor-1");
|
|
assert_eq!(handler.connection_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_client_creation() {
|
|
let handler = IbcHandler::default();
|
|
|
|
let client_state = ClientState::new_synor(
|
|
ChainId::synor_testnet(),
|
|
Height::from_height(100),
|
|
);
|
|
let consensus_state = ConsensusState::new(
|
|
Timestamp::now(),
|
|
vec![0u8; 32],
|
|
vec![0u8; 32],
|
|
);
|
|
|
|
let client_id = handler.create_client(client_state, consensus_state).unwrap();
|
|
assert!(handler.get_client_state(&client_id).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_connection_init() {
|
|
let handler = setup_handler();
|
|
|
|
let client_id = ClientId::new("synor", 0);
|
|
let counterparty_client = ClientId::new("07-tendermint", 0);
|
|
|
|
let conn_id = handler.connection_open_init(
|
|
client_id,
|
|
counterparty_client,
|
|
None,
|
|
).unwrap();
|
|
|
|
let conn = handler.get_connection(&conn_id).unwrap();
|
|
assert!(!conn.is_open()); // Still in Init state
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_block() {
|
|
let handler = IbcHandler::default();
|
|
|
|
let new_height = Height::from_height(100);
|
|
let new_time = Timestamp::from_seconds(1000);
|
|
|
|
handler.update_block(new_height, new_time);
|
|
|
|
assert_eq!(handler.current_height(), new_height);
|
|
assert_eq!(handler.current_timestamp(), new_time);
|
|
}
|
|
}
|