diff --git a/Cargo.toml b/Cargo.toml index 10592a8..9b84b06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/synor-mining", "crates/synor-zk", "crates/synor-ibc", + "crates/synor-bridge", "crates/synor-privacy", "crates/synor-sharding", "crates/synor-verifier", diff --git a/crates/synor-bridge/Cargo.toml b/crates/synor-bridge/Cargo.toml new file mode 100644 index 0000000..dc0ce50 --- /dev/null +++ b/crates/synor-bridge/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "synor-bridge" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "Cross-chain bridge infrastructure for Synor blockchain" + +[dependencies] +synor-types = { path = "../synor-types" } +synor-crypto = { path = "../synor-crypto" } +synor-ibc = { path = "../synor-ibc" } + +# Serialization +serde = { workspace = true } +serde_json = { workspace = true } +borsh = { workspace = true } + +# Cryptography +sha2 = "0.10" +sha3 = { workspace = true } +blake3 = { workspace = true } +rand = { workspace = true } + +# Ethereum compatibility +alloy-primitives = { version = "0.8", features = ["serde"] } +alloy-sol-types = "0.8" + +# Async runtime +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" + +# Utilities +thiserror = { workspace = true } +hex = { workspace = true } +parking_lot = { workspace = true } +tracing = "0.1" +chrono = { workspace = true } + +[dev-dependencies] +tokio-test = "0.4" +proptest = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/synor-bridge/src/ethereum.rs b/crates/synor-bridge/src/ethereum.rs new file mode 100644 index 0000000..6299ab4 --- /dev/null +++ b/crates/synor-bridge/src/ethereum.rs @@ -0,0 +1,808 @@ +//! Ethereum Bridge Implementation +//! +//! Lock-Mint bridge for Ethereum assets: +//! - ETH → sETH (wrapped ETH on Synor) +//! - ERC-20 → sERC20 (wrapped tokens) +//! +//! # Lock-Mint Flow +//! +//! 1. User locks ETH/ERC-20 in vault contract on Ethereum +//! 2. Relayer detects lock event and submits proof to Synor +//! 3. Synor verifies proof and mints wrapped tokens +//! +//! # Burn-Unlock Flow +//! +//! 1. User burns wrapped tokens on Synor +//! 2. Relayer detects burn event and submits proof to Ethereum +//! 3. Vault contract verifies proof and unlocks original tokens + +use crate::{ + AssetId, Bridge, BridgeAddress, BridgeError, BridgeResult, BridgeTransfer, ChainType, + TransferDirection, TransferId, TransferManager, TransferStatus, VaultManager, + ETH_MIN_CONFIRMATIONS, +}; +use alloy_primitives::{Address, B256, U256}; +use alloy_sol_types::sol; +use async_trait::async_trait; +use borsh::{BorshDeserialize, BorshSerialize}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Keccak256}; +use std::collections::HashMap; +use std::sync::Arc; + +// Solidity event signatures +sol! { + event TokenLocked( + address indexed token, + address indexed sender, + bytes32 recipient, + uint256 amount, + uint256 nonce + ); + + event TokenUnlocked( + address indexed token, + address indexed recipient, + uint256 amount, + bytes32 transferId + ); +} + +/// Ethereum bridge configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EthereumBridgeConfig { + /// Ethereum chain ID + pub chain_id: u64, + /// Vault contract address + pub vault_address: Address, + /// Required confirmations + pub required_confirmations: u64, + /// Supported tokens (address → AssetId) + pub supported_tokens: HashMap, + /// Relayer addresses (for multi-sig) + pub relayers: Vec
, + /// Required relayer signatures + pub required_signatures: usize, + /// Whether bridge is paused + pub paused: bool, + /// Daily limit per token (address → limit) + pub daily_limits: HashMap, +} + +impl Default for EthereumBridgeConfig { + fn default() -> Self { + let mut supported_tokens = HashMap::new(); + supported_tokens.insert(Address::ZERO, AssetId::eth()); // Native ETH + + Self { + chain_id: 1, // Mainnet + vault_address: Address::ZERO, + required_confirmations: ETH_MIN_CONFIRMATIONS, + supported_tokens, + relayers: Vec::new(), + required_signatures: 1, + paused: false, + daily_limits: HashMap::new(), + } + } +} + +impl EthereumBridgeConfig { + /// Create testnet config + pub fn sepolia() -> Self { + Self { + chain_id: 11155111, + required_confirmations: 3, // Lower for testnet + ..Default::default() + } + } + + /// Add supported token + pub fn add_token(&mut self, address: Address, asset: AssetId) { + self.supported_tokens.insert(address, asset); + } + + /// Add relayer + pub fn add_relayer(&mut self, address: Address) { + if !self.relayers.contains(&address) { + self.relayers.push(address); + } + } +} + +/// Ethereum event types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EthereumEventType { + /// Token locked for bridging to Synor + TokenLocked, + /// Token unlocked after bridging from Synor + TokenUnlocked, +} + +/// Ethereum event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EthereumEvent { + /// Event type + pub event_type: EthereumEventType, + /// Transaction hash + pub tx_hash: B256, + /// Block number + pub block_number: u64, + /// Log index + pub log_index: u64, + /// Token address (zero for native ETH) + pub token: Address, + /// Sender address + pub sender: Address, + /// Amount + pub amount: U256, + /// Recipient (Synor address for lock, Ethereum address for unlock) + pub recipient: Vec, + /// Nonce + pub nonce: u64, +} + +impl EthereumEvent { + /// Compute event hash for verification + pub fn hash(&self) -> B256 { + let mut hasher = Keccak256::new(); + hasher.update(self.tx_hash.as_slice()); + hasher.update(&self.block_number.to_le_bytes()); + hasher.update(&self.log_index.to_le_bytes()); + hasher.update(self.token.as_slice()); + hasher.update(self.sender.as_slice()); + hasher.update(&self.amount.to_le_bytes::<32>()); + hasher.update(&self.recipient); + hasher.update(&self.nonce.to_le_bytes()); + + let result = hasher.finalize(); + B256::from_slice(&result) + } +} + +/// Wrapped token on Synor +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct WrappedToken { + /// Original asset + pub original: AssetId, + /// Wrapped asset on Synor + pub wrapped: AssetId, + /// Total supply minted + pub total_supply: u128, + /// Original token contract on Ethereum + pub ethereum_address: Vec, +} + +impl WrappedToken { + /// Create a wrapped token + pub fn new(ethereum_address: Address, original: AssetId) -> Self { + let wrapped = AssetId::wrapped(&original); + Self { + original, + wrapped, + total_supply: 0, + ethereum_address: ethereum_address.to_vec(), + } + } + + /// Mint wrapped tokens + pub fn mint(&mut self, amount: u128) { + self.total_supply += amount; + } + + /// Burn wrapped tokens + pub fn burn(&mut self, amount: u128) -> BridgeResult<()> { + if amount > self.total_supply { + return Err(BridgeError::InsufficientBalance { + required: amount, + available: self.total_supply, + }); + } + self.total_supply -= amount; + Ok(()) + } +} + +/// Ethereum bridge +pub struct EthereumBridge { + /// Configuration + config: RwLock, + /// Transfer manager + transfers: Arc>, + /// Vault manager + vaults: Arc>, + /// Wrapped tokens by original address + wrapped_tokens: RwLock>, + /// Pending events awaiting confirmation + pending_events: RwLock>, + /// Processed event hashes (to prevent replay) + processed_events: RwLock>, + /// Relayer signatures per event + relayer_signatures: RwLock)>>>, +} + +impl EthereumBridge { + /// Create a new Ethereum bridge + pub fn new(config: EthereumBridgeConfig) -> Self { + Self { + config: RwLock::new(config), + transfers: Arc::new(RwLock::new(TransferManager::new())), + vaults: Arc::new(RwLock::new(VaultManager::new())), + wrapped_tokens: RwLock::new(HashMap::new()), + pending_events: RwLock::new(HashMap::new()), + processed_events: RwLock::new(HashMap::new()), + relayer_signatures: RwLock::new(HashMap::new()), + } + } + + /// Create for Sepolia testnet + pub fn sepolia() -> Self { + Self::new(EthereumBridgeConfig::sepolia()) + } + + /// Get configuration + pub fn config(&self) -> EthereumBridgeConfig { + self.config.read().clone() + } + + /// Update configuration + pub fn update_config(&self, f: F) + where + F: FnOnce(&mut EthereumBridgeConfig), + { + f(&mut self.config.write()); + } + + /// Pause bridge + pub fn pause(&self) { + self.config.write().paused = true; + } + + /// Resume bridge + pub fn resume(&self) { + self.config.write().paused = false; + } + + /// Check if bridge is paused + pub fn is_paused(&self) -> bool { + self.config.read().paused + } + + /// Process a lock event from Ethereum + pub fn process_lock_event( + &self, + event: EthereumEvent, + current_time: u64, + ) -> BridgeResult { + if self.is_paused() { + return Err(BridgeError::BridgePaused); + } + + // Check for replay + let event_hash = event.hash(); + if self.processed_events.read().contains_key(&event_hash) { + return Err(BridgeError::TransferAlreadyExists( + hex::encode(event_hash.as_slice()), + )); + } + + // Verify token is supported + let config = self.config.read(); + let asset = config + .supported_tokens + .get(&event.token) + .cloned() + .ok_or_else(|| BridgeError::AssetNotSupported(format!("{:?}", event.token)))?; + + // Create bridge address from recipient bytes + let recipient = if event.recipient.len() == 32 { + let mut arr = [0u8; 32]; + arr.copy_from_slice(&event.recipient); + BridgeAddress::from_synor(arr) + } else { + return Err(BridgeError::InvalidAddress("invalid recipient".to_string())); + }; + + let sender = BridgeAddress::from_eth(event.sender.into()); + let amount = event.amount.to::(); + + // Create transfer + let transfer_id = self.transfers.write().create_inbound( + ChainType::Ethereum, + asset, + amount, + sender, + recipient, + config.required_confirmations, + current_time, + )?; + + // Store pending event + self.pending_events.write().insert(event_hash, event); + + // Record lock in transfer + self.transfers.write().confirm_lock( + &transfer_id, + event_hash.to_vec(), + 0, // Will be updated with actual block number + current_time, + )?; + + Ok(transfer_id) + } + + /// Submit relayer signature for an event + pub fn submit_relayer_signature( + &self, + event_hash: B256, + relayer: Address, + signature: Vec, + ) -> BridgeResult { + let config = self.config.read(); + + // Verify relayer is authorized + if !config.relayers.contains(&relayer) { + return Err(BridgeError::SignatureVerificationFailed( + "unauthorized relayer".to_string(), + )); + } + + // Add signature + let mut signatures = self.relayer_signatures.write(); + let sigs = signatures.entry(event_hash).or_default(); + + // Check for duplicate + if sigs.iter().any(|(r, _)| r == &relayer) { + return Ok(false); + } + + sigs.push((relayer, signature)); + + // Check if we have enough signatures + Ok(sigs.len() >= config.required_signatures) + } + + /// Update confirmations for pending events + pub fn update_confirmations( + &self, + current_block: u64, + current_time: u64, + ) -> BridgeResult> { + let config = self.config.read(); + let required_confirmations = config.required_confirmations; + drop(config); + + let mut confirmed = Vec::new(); + + // Collect pending events and their confirmations + let pending = self.pending_events.read(); + let events_to_process: Vec<_> = pending + .iter() + .map(|(hash, event)| { + let confirmations = current_block.saturating_sub(event.block_number); + (*hash, confirmations) + }) + .collect(); + drop(pending); + + // Find and update matching transfers + for (event_hash, confirmations) in events_to_process { + // Collect matching transfer IDs first + let matching_transfer_id = { + let transfers = self.transfers.read(); + transfers + .pending_transfers() + .iter() + .find_map(|transfer| { + transfer.source_tx_hash.as_ref().and_then(|tx_hash| { + if tx_hash.as_slice() == event_hash.as_slice() { + Some(transfer.id.clone()) + } else { + None + } + }) + }) + }; + + // Now update the transfer if found + if let Some(transfer_id) = matching_transfer_id { + self.transfers.write().update_confirmations( + &transfer_id, + confirmations, + current_time, + )?; + + if confirmations >= required_confirmations { + confirmed.push(transfer_id); + } + } + } + + Ok(confirmed) + } + + /// Mint wrapped tokens after confirmation + pub fn mint_wrapped_tokens( + &self, + transfer_id: &TransferId, + current_time: u64, + ) -> BridgeResult<()> { + let mut transfers = self.transfers.write(); + let transfer = transfers + .get(transfer_id) + .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?; + + // Verify transfer is confirmed + if transfer.status != TransferStatus::Confirmed { + return Err(BridgeError::InvalidProof(format!( + "transfer not confirmed: {}", + transfer.status + ))); + } + + // Get original token address + let token_address = if transfer.asset.identifier == "native" { + Address::ZERO + } else { + let bytes = hex::decode( + transfer + .asset + .identifier + .strip_prefix("0x") + .unwrap_or(&transfer.asset.identifier), + ) + .map_err(|e| BridgeError::InvalidAddress(e.to_string()))?; + + if bytes.len() != 20 { + return Err(BridgeError::InvalidAddress("invalid address length".to_string())); + } + Address::from_slice(&bytes) + }; + + // Get or create wrapped token + let mut wrapped_tokens = self.wrapped_tokens.write(); + let wrapped = wrapped_tokens + .entry(token_address) + .or_insert_with(|| WrappedToken::new(token_address, transfer.asset.clone())); + + // Mint tokens + wrapped.mint(transfer.amount); + + // Mark transfer as completed + drop(wrapped_tokens); + transfers.confirm_mint(transfer_id, vec![], current_time)?; + + Ok(()) + } + + /// Initiate burn for outbound transfer + pub fn initiate_burn( + &self, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + current_time: u64, + ) -> BridgeResult { + if self.is_paused() { + return Err(BridgeError::BridgePaused); + } + + // Find wrapped token + let token_address = if let Some(id) = asset.unwrap_identifier() { + if id == "native" { + Address::ZERO + } else { + let bytes = hex::decode(id.strip_prefix("0x").unwrap_or(id)) + .map_err(|e| BridgeError::InvalidAddress(e.to_string()))?; + Address::from_slice(&bytes) + } + } else { + return Err(BridgeError::AssetNotSupported(asset.to_string())); + }; + + // Verify we have enough supply + let mut wrapped_tokens = self.wrapped_tokens.write(); + let wrapped = wrapped_tokens + .get_mut(&token_address) + .ok_or_else(|| BridgeError::AssetNotSupported(format!("{:?}", token_address)))?; + + // Burn tokens + wrapped.burn(amount)?; + + // Get original asset + let original_asset = wrapped.original.clone(); + + // Create outbound transfer + let config = self.config.read(); + let transfer_id = self.transfers.write().create_outbound( + ChainType::Ethereum, + original_asset, + amount, + sender, + recipient, + config.required_confirmations, + current_time, + )?; + + Ok(transfer_id) + } + + /// Get wrapped token info + pub fn get_wrapped_token(&self, address: Address) -> Option { + self.wrapped_tokens.read().get(&address).cloned() + } + + /// Get total wrapped supply + pub fn total_wrapped_supply(&self) -> u128 { + self.wrapped_tokens + .read() + .values() + .map(|t| t.total_supply) + .sum() + } + + /// Get transfer manager + pub fn transfer_manager(&self) -> Arc> { + self.transfers.clone() + } + + /// Get vault manager + pub fn vault_manager(&self) -> Arc> { + self.vaults.clone() + } +} + +#[async_trait] +impl Bridge for EthereumBridge { + fn source_chain(&self) -> ChainType { + ChainType::Ethereum + } + + fn destination_chain(&self) -> ChainType { + ChainType::Synor + } + + fn supports_asset(&self, asset: &AssetId) -> bool { + let config = self.config.read(); + config + .supported_tokens + .values() + .any(|a| a.identifier == asset.identifier) + } + + fn min_confirmations(&self) -> u64 { + self.config.read().required_confirmations + } + + async fn lock(&self, transfer: &BridgeTransfer) -> BridgeResult { + // In production, this would interact with Ethereum contract + // For now, we simulate the lock + Ok(transfer.id.clone()) + } + + async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult { + let transfers = self.transfers.read(); + let transfer = transfers + .get(transfer_id) + .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?; + + Ok(transfer.has_sufficient_confirmations()) + } + + async fn mint(&self, transfer: &BridgeTransfer) -> BridgeResult<()> { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + self.mint_wrapped_tokens(&transfer.id, current_time) + } + + async fn burn(&self, transfer: &BridgeTransfer) -> BridgeResult { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + self.initiate_burn( + transfer.asset.clone(), + transfer.amount, + transfer.sender.clone(), + transfer.recipient.clone(), + current_time, + ) + } + + async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()> { + // In production, this would interact with Ethereum contract + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + self.transfers + .write() + .confirm_unlock(transfer_id, vec![], current_time) + } + + async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult { + let transfers = self.transfers.read(); + let transfer = transfers + .get(transfer_id) + .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?; + + Ok(transfer.status) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_sender() -> BridgeAddress { + BridgeAddress::from_eth([0xaa; 20]) + } + + fn test_recipient() -> BridgeAddress { + BridgeAddress::from_synor([0xbb; 32]) + } + + #[test] + fn test_bridge_creation() { + let bridge = EthereumBridge::new(EthereumBridgeConfig::default()); + assert!(!bridge.is_paused()); + assert!(bridge.supports_asset(&AssetId::eth())); + } + + #[test] + fn test_lock_event_processing() { + let bridge = EthereumBridge::new(EthereumBridgeConfig::default()); + let current_time = 1700000000; + + let event = EthereumEvent { + event_type: EthereumEventType::TokenLocked, + tx_hash: B256::from([0x11; 32]), + block_number: 100, + log_index: 0, + token: Address::ZERO, // Native ETH + sender: Address::from([0xaa; 20]), + amount: U256::from(1000u64), + recipient: vec![0xbb; 32], + nonce: 0, + }; + + let transfer_id = bridge.process_lock_event(event.clone(), current_time).unwrap(); + + // Verify transfer was created + let transfers = bridge.transfers.read(); + let transfer = transfers.get(&transfer_id).unwrap(); + assert_eq!(transfer.direction, TransferDirection::Inbound); + assert_eq!(transfer.amount, 1000); + } + + #[test] + fn test_wrapped_token_minting() { + let bridge = EthereumBridge::new(EthereumBridgeConfig::default()); + let current_time = 1700000000; + + // Process lock event + let event = EthereumEvent { + event_type: EthereumEventType::TokenLocked, + tx_hash: B256::from([0x11; 32]), + block_number: 100, + log_index: 0, + token: Address::ZERO, + sender: Address::from([0xaa; 20]), + amount: U256::from(1000u64), + recipient: vec![0xbb; 32], + nonce: 0, + }; + + let transfer_id = bridge.process_lock_event(event, current_time).unwrap(); + + // Simulate confirmations + bridge + .transfers + .write() + .update_confirmations(&transfer_id, 12, current_time + 100) + .unwrap(); + + // Mint wrapped tokens + bridge.mint_wrapped_tokens(&transfer_id, current_time + 200).unwrap(); + + // Verify wrapped tokens were minted + let wrapped = bridge.get_wrapped_token(Address::ZERO).unwrap(); + assert_eq!(wrapped.total_supply, 1000); + } + + #[test] + fn test_burn_initiation() { + let bridge = EthereumBridge::new(EthereumBridgeConfig::default()); + let current_time = 1700000000; + + // First mint some wrapped tokens + let mut wrapped_tokens = bridge.wrapped_tokens.write(); + let mut wrapped = WrappedToken::new(Address::ZERO, AssetId::eth()); + wrapped.mint(5000); + wrapped_tokens.insert(Address::ZERO, wrapped); + drop(wrapped_tokens); + + // Initiate burn + let asset = AssetId::wrapped(&AssetId::eth()); + let transfer_id = bridge + .initiate_burn( + asset, + 1000, + test_recipient(), + test_sender(), + current_time, + ) + .unwrap(); + + // Verify transfer was created + let transfers = bridge.transfers.read(); + let transfer = transfers.get(&transfer_id).unwrap(); + assert_eq!(transfer.direction, TransferDirection::Outbound); + assert_eq!(transfer.amount, 1000); + + // Verify wrapped supply decreased + drop(transfers); + let wrapped = bridge.get_wrapped_token(Address::ZERO).unwrap(); + assert_eq!(wrapped.total_supply, 4000); + } + + #[test] + fn test_bridge_pause() { + let bridge = EthereumBridge::new(EthereumBridgeConfig::default()); + + bridge.pause(); + assert!(bridge.is_paused()); + + let event = EthereumEvent { + event_type: EthereumEventType::TokenLocked, + tx_hash: B256::from([0x11; 32]), + block_number: 100, + log_index: 0, + token: Address::ZERO, + sender: Address::from([0xaa; 20]), + amount: U256::from(1000u64), + recipient: vec![0xbb; 32], + nonce: 0, + }; + + let result = bridge.process_lock_event(event, 0); + assert!(matches!(result, Err(BridgeError::BridgePaused))); + + bridge.resume(); + assert!(!bridge.is_paused()); + } + + #[test] + fn test_event_hash() { + let event1 = EthereumEvent { + event_type: EthereumEventType::TokenLocked, + tx_hash: B256::from([0x11; 32]), + block_number: 100, + log_index: 0, + token: Address::ZERO, + sender: Address::from([0xaa; 20]), + amount: U256::from(1000u64), + recipient: vec![0xbb; 32], + nonce: 0, + }; + + let event2 = EthereumEvent { + nonce: 1, + ..event1.clone() + }; + + // Different nonce should produce different hash + assert_ne!(event1.hash(), event2.hash()); + + // Same event should produce same hash + let event3 = event1.clone(); + assert_eq!(event1.hash(), event3.hash()); + } +} diff --git a/crates/synor-bridge/src/lib.rs b/crates/synor-bridge/src/lib.rs new file mode 100644 index 0000000..6d1b0c2 --- /dev/null +++ b/crates/synor-bridge/src/lib.rs @@ -0,0 +1,436 @@ +//! Cross-Chain Bridge Infrastructure for Synor +//! +//! This crate provides bridge infrastructure for cross-chain asset transfers, +//! enabling Synor to interoperate with external blockchains like Ethereum. +//! +//! # Architecture +//! +//! ```text +//! ┌────────────────────────────────────────────────────────────────────┐ +//! │ Synor Bridge Architecture │ +//! ├────────────────────────────────────────────────────────────────────┤ +//! │ │ +//! │ ┌──────────────┐ Lock-Mint ┌──────────────────────────┐ │ +//! │ │ External │ ──────────────► │ Synor Chain │ │ +//! │ │ Chain │ │ │ │ +//! │ │ (Ethereum) │ ◄────────────── │ Wrapped Tokens (sETH) │ │ +//! │ │ │ Burn-Unlock │ │ │ +//! │ └──────────────┘ └──────────────────────────┘ │ +//! │ │ +//! │ ┌─────────────────────────────────────────────────────────────┐ │ +//! │ │ Bridge Components │ │ +//! │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐ │ │ +//! │ │ │ Vault │ │ Relayer │ │ Validator │ │ Oracle │ │ │ +//! │ │ │ (Locks) │ │ (Events) │ │ (Proofs) │ │ (Price) │ │ │ +//! │ │ └───────────┘ └───────────┘ └───────────┘ └──────────┘ │ │ +//! │ └─────────────────────────────────────────────────────────────┘ │ +//! │ │ +//! └────────────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! # Bridge Flow (Lock-Mint) +//! +//! ```text +//! User Ethereum Relayer Synor +//! │ │ │ │ +//! ├── Lock ETH ─────────►│ │ │ +//! │ ├── LockEvent ─────►│ │ +//! │ │ ├── SubmitProof ───►│ +//! │ │ │ ├─ Verify +//! │ │ │ ├─ Mint sETH +//! │◄─────────────────────────────────────────────────── sETH ──────┤ +//! ``` +//! +//! # Supported Bridges +//! +//! - **Ethereum**: Lock-Mint bridge for ETH and ERC-20 tokens +//! - **Bitcoin**: HTLC-based atomic swaps (via synor-ibc) +//! - **IBC Chains**: Native IBC transfers (via synor-ibc) + +#![allow(dead_code)] + +pub mod ethereum; +pub mod transfer; +pub mod vault; + +pub use ethereum::{ + EthereumBridge, EthereumBridgeConfig, EthereumEvent, EthereumEventType, WrappedToken, +}; +pub use transfer::{ + BridgeTransfer, TransferDirection, TransferId, TransferManager, TransferStatus, +}; +pub use vault::{LockedAsset, Vault, VaultId, VaultManager}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use thiserror::Error; + +/// Bridge protocol version +pub const BRIDGE_VERSION: &str = "1.0.0"; + +/// Maximum transfer amount for safety (in smallest units) +pub const MAX_TRANSFER_AMOUNT: u128 = 1_000_000_000_000_000_000_000_000; // 1M tokens + +/// Minimum confirmations for Ethereum deposits +pub const ETH_MIN_CONFIRMATIONS: u64 = 12; + +/// Minimum confirmations for Bitcoin deposits +pub const BTC_MIN_CONFIRMATIONS: u64 = 6; + +/// Bridge chain identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum ChainType { + /// Synor native chain + Synor, + /// Ethereum mainnet + Ethereum, + /// Ethereum Sepolia testnet + EthereumSepolia, + /// Bitcoin mainnet + Bitcoin, + /// Bitcoin testnet + BitcoinTestnet, + /// Cosmos SDK chain (IBC) + Cosmos(String), + /// Custom chain + Custom(String), +} + +impl ChainType { + /// Get chain ID for Ethereum networks + pub fn eth_chain_id(&self) -> Option { + match self { + ChainType::Ethereum => Some(1), + ChainType::EthereumSepolia => Some(11155111), + _ => None, + } + } + + /// Check if this is an EVM-compatible chain + pub fn is_evm(&self) -> bool { + matches!(self, ChainType::Ethereum | ChainType::EthereumSepolia) + } +} + +impl fmt::Display for ChainType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ChainType::Synor => write!(f, "synor"), + ChainType::Ethereum => write!(f, "ethereum"), + ChainType::EthereumSepolia => write!(f, "ethereum-sepolia"), + ChainType::Bitcoin => write!(f, "bitcoin"), + ChainType::BitcoinTestnet => write!(f, "bitcoin-testnet"), + ChainType::Cosmos(id) => write!(f, "cosmos:{}", id), + ChainType::Custom(id) => write!(f, "custom:{}", id), + } + } +} + +/// Asset identifier across chains +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AssetId { + /// Chain where the asset originates + pub chain: ChainType, + /// Asset identifier (contract address for ERC-20, "native" for ETH) + pub identifier: String, + /// Asset symbol + pub symbol: String, + /// Decimal places + pub decimals: u8, +} + +impl AssetId { + /// Create native Ethereum asset + pub fn eth() -> Self { + Self { + chain: ChainType::Ethereum, + identifier: "native".to_string(), + symbol: "ETH".to_string(), + decimals: 18, + } + } + + /// Create ERC-20 asset + pub fn erc20(address: impl Into, symbol: impl Into, decimals: u8) -> Self { + Self { + chain: ChainType::Ethereum, + identifier: address.into(), + symbol: symbol.into(), + decimals, + } + } + + /// Create Synor native asset + pub fn synor() -> Self { + Self { + chain: ChainType::Synor, + identifier: "native".to_string(), + symbol: "SYNOR".to_string(), + decimals: 18, + } + } + + /// Create wrapped asset on Synor + pub fn wrapped(original: &AssetId) -> Self { + Self { + chain: ChainType::Synor, + identifier: format!("wrapped:{}", original.identifier), + symbol: format!("s{}", original.symbol), + decimals: original.decimals, + } + } + + /// Check if this is a wrapped asset + pub fn is_wrapped(&self) -> bool { + self.identifier.starts_with("wrapped:") + } + + /// Get the original asset if this is wrapped + pub fn unwrap_identifier(&self) -> Option<&str> { + self.identifier.strip_prefix("wrapped:") + } +} + +impl fmt::Display for AssetId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.chain, self.symbol) + } +} + +/// Bridge address (unified format for cross-chain addresses) +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct BridgeAddress { + /// Chain type + pub chain: ChainType, + /// Address bytes + pub address: Vec, +} + +impl BridgeAddress { + /// Create from Ethereum address (20 bytes) + pub fn from_eth(address: [u8; 20]) -> Self { + Self { + chain: ChainType::Ethereum, + address: address.to_vec(), + } + } + + /// Create from Synor address (32 bytes) + pub fn from_synor(address: [u8; 32]) -> Self { + Self { + chain: ChainType::Synor, + address: address.to_vec(), + } + } + + /// Create from hex string + pub fn from_hex(chain: ChainType, hex: &str) -> Result { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + let address = hex::decode(hex) + .map_err(|e| BridgeError::InvalidAddress(format!("invalid hex: {}", e)))?; + Ok(Self { chain, address }) + } + + /// Get as Ethereum address + pub fn as_eth(&self) -> Option<[u8; 20]> { + if self.address.len() == 20 { + let mut arr = [0u8; 20]; + arr.copy_from_slice(&self.address); + Some(arr) + } else { + None + } + } + + /// Get as Synor address + pub fn as_synor(&self) -> Option<[u8; 32]> { + if self.address.len() == 32 { + let mut arr = [0u8; 32]; + arr.copy_from_slice(&self.address); + Some(arr) + } else { + None + } + } + + /// Get as hex string + pub fn to_hex(&self) -> String { + format!("0x{}", hex::encode(&self.address)) + } +} + +impl fmt::Display for BridgeAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.chain, self.to_hex()) + } +} + +/// Bridge error types +#[derive(Debug, Error)] +pub enum BridgeError { + #[error("Invalid address: {0}")] + InvalidAddress(String), + + #[error("Invalid amount: {0}")] + InvalidAmount(String), + + #[error("Transfer not found: {0}")] + TransferNotFound(String), + + #[error("Vault not found: {0}")] + VaultNotFound(String), + + #[error("Insufficient balance: need {required}, have {available}")] + InsufficientBalance { required: u128, available: u128 }, + + #[error("Asset not supported: {0}")] + AssetNotSupported(String), + + #[error("Chain not supported: {0}")] + ChainNotSupported(String), + + #[error("Transfer already exists: {0}")] + TransferAlreadyExists(String), + + #[error("Transfer already completed: {0}")] + TransferAlreadyCompleted(String), + + #[error("Invalid proof: {0}")] + InvalidProof(String), + + #[error("Insufficient confirmations: need {required}, have {actual}")] + InsufficientConfirmations { required: u64, actual: u64 }, + + #[error("Bridge paused")] + BridgePaused, + + #[error("Rate limit exceeded")] + RateLimitExceeded, + + #[error("Signature verification failed: {0}")] + SignatureVerificationFailed(String), + + #[error("Oracle error: {0}")] + OracleError(String), + + #[error("Relayer error: {0}")] + RelayerError(String), + + #[error("Internal error: {0}")] + Internal(String), +} + +/// Result type for bridge operations +pub type BridgeResult = std::result::Result; + +/// Bridge trait for implementing cross-chain bridges +#[async_trait::async_trait] +pub trait Bridge: Send + Sync { + /// Get the source chain type + fn source_chain(&self) -> ChainType; + + /// Get the destination chain type + fn destination_chain(&self) -> ChainType; + + /// Check if an asset is supported + fn supports_asset(&self, asset: &AssetId) -> bool; + + /// Get minimum confirmations required + fn min_confirmations(&self) -> u64; + + /// Lock assets on the source chain + async fn lock(&self, transfer: &BridgeTransfer) -> BridgeResult; + + /// Verify a lock proof + async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult; + + /// Mint wrapped tokens on the destination chain + async fn mint(&self, transfer: &BridgeTransfer) -> BridgeResult<()>; + + /// Burn wrapped tokens (for redemption) + async fn burn(&self, transfer: &BridgeTransfer) -> BridgeResult; + + /// Unlock original tokens + async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()>; + + /// Get transfer status + async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult; +} + +/// Bridge event for tracking cross-chain activity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BridgeEvent { + /// Asset locked on source chain + AssetLocked { + transfer_id: TransferId, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + }, + /// Lock verified + LockVerified { + transfer_id: TransferId, + confirmations: u64, + }, + /// Wrapped token minted + TokenMinted { + transfer_id: TransferId, + asset: AssetId, + amount: u128, + recipient: BridgeAddress, + }, + /// Token burned for redemption + TokenBurned { + transfer_id: TransferId, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + }, + /// Original asset unlocked + AssetUnlocked { + transfer_id: TransferId, + recipient: BridgeAddress, + }, + /// Transfer completed + TransferCompleted { transfer_id: TransferId }, + /// Transfer failed + TransferFailed { + transfer_id: TransferId, + reason: String, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chain_type() { + assert!(ChainType::Ethereum.is_evm()); + assert!(!ChainType::Bitcoin.is_evm()); + assert_eq!(ChainType::Ethereum.eth_chain_id(), Some(1)); + } + + #[test] + fn test_asset_id() { + let eth = AssetId::eth(); + assert_eq!(eth.symbol, "ETH"); + assert_eq!(eth.decimals, 18); + + let wrapped = AssetId::wrapped(ð); + assert!(wrapped.is_wrapped()); + assert_eq!(wrapped.symbol, "sETH"); + } + + #[test] + fn test_bridge_address() { + let eth_addr = BridgeAddress::from_eth([0xde; 20]); + assert_eq!(eth_addr.address.len(), 20); + assert!(eth_addr.as_eth().is_some()); + + let hex_addr = BridgeAddress::from_hex(ChainType::Ethereum, "0xdeadbeef").unwrap(); + assert_eq!(hex_addr.address, vec![0xde, 0xad, 0xbe, 0xef]); + } +} diff --git a/crates/synor-bridge/src/transfer.rs b/crates/synor-bridge/src/transfer.rs new file mode 100644 index 0000000..b05d148 --- /dev/null +++ b/crates/synor-bridge/src/transfer.rs @@ -0,0 +1,830 @@ +//! Bridge Transfer Management +//! +//! Manages cross-chain transfer lifecycle including: +//! - Transfer initiation +//! - Proof verification +//! - Status tracking +//! - Completion and failure handling + +use crate::{AssetId, BridgeAddress, BridgeError, BridgeResult, ChainType}; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::fmt; + +/// Unique transfer identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct TransferId(pub String); + +impl TransferId { + /// Create a new transfer ID + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + /// Generate transfer ID from parameters + pub fn generate( + sender: &BridgeAddress, + recipient: &BridgeAddress, + asset: &AssetId, + amount: u128, + nonce: u64, + ) -> Self { + let mut hasher = Sha256::new(); + hasher.update(&sender.address); + hasher.update(&recipient.address); + hasher.update(asset.identifier.as_bytes()); + hasher.update(&amount.to_le_bytes()); + hasher.update(&nonce.to_le_bytes()); + Self(hex::encode(&hasher.finalize()[..16])) + } +} + +impl fmt::Display for TransferId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Transfer direction +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum TransferDirection { + /// From external chain to Synor (Lock → Mint) + Inbound, + /// From Synor to external chain (Burn → Unlock) + Outbound, +} + +impl fmt::Display for TransferDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TransferDirection::Inbound => write!(f, "inbound"), + TransferDirection::Outbound => write!(f, "outbound"), + } + } +} + +/// Transfer status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum TransferStatus { + /// Transfer initiated, awaiting lock confirmation + Pending, + /// Lock confirmed, awaiting sufficient confirmations + Locked, + /// Sufficient confirmations, ready for minting/unlocking + Confirmed, + /// Tokens minted on destination (inbound) or burned (outbound) + Minted, + /// Original tokens unlocked (outbound only) + Unlocked, + /// Transfer completed successfully + Completed, + /// Transfer failed + Failed, + /// Transfer expired + Expired, + /// Transfer refunded + Refunded, +} + +impl TransferStatus { + /// Check if transfer is finalized + pub fn is_finalized(&self) -> bool { + matches!( + self, + TransferStatus::Completed + | TransferStatus::Failed + | TransferStatus::Expired + | TransferStatus::Refunded + ) + } + + /// Check if transfer can be retried + pub fn can_retry(&self) -> bool { + matches!( + self, + TransferStatus::Pending | TransferStatus::Failed | TransferStatus::Expired + ) + } +} + +impl fmt::Display for TransferStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TransferStatus::Pending => write!(f, "pending"), + TransferStatus::Locked => write!(f, "locked"), + TransferStatus::Confirmed => write!(f, "confirmed"), + TransferStatus::Minted => write!(f, "minted"), + TransferStatus::Unlocked => write!(f, "unlocked"), + TransferStatus::Completed => write!(f, "completed"), + TransferStatus::Failed => write!(f, "failed"), + TransferStatus::Expired => write!(f, "expired"), + TransferStatus::Refunded => write!(f, "refunded"), + } + } +} + +/// Cross-chain bridge transfer +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct BridgeTransfer { + /// Unique transfer ID + pub id: TransferId, + /// Transfer direction + pub direction: TransferDirection, + /// Source chain + pub source_chain: ChainType, + /// Destination chain + pub destination_chain: ChainType, + /// Asset being transferred + pub asset: AssetId, + /// Amount in smallest unit + pub amount: u128, + /// Sender address on source chain + pub sender: BridgeAddress, + /// Recipient address on destination chain + pub recipient: BridgeAddress, + /// Current status + pub status: TransferStatus, + /// Source chain transaction hash + pub source_tx_hash: Option>, + /// Destination chain transaction hash + pub destination_tx_hash: Option>, + /// Block number where lock occurred + pub lock_block: Option, + /// Current confirmations + pub confirmations: u64, + /// Required confirmations + pub required_confirmations: u64, + /// Transfer initiation timestamp + pub created_at: u64, + /// Last update timestamp + pub updated_at: u64, + /// Expiry timestamp (0 = no expiry) + pub expires_at: u64, + /// Error message if failed + pub error: Option, + /// Nonce for uniqueness + pub nonce: u64, +} + +impl BridgeTransfer { + /// Create a new transfer + pub fn new( + direction: TransferDirection, + source_chain: ChainType, + destination_chain: ChainType, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + required_confirmations: u64, + nonce: u64, + current_time: u64, + ) -> Self { + let id = TransferId::generate(&sender, &recipient, &asset, amount, nonce); + + Self { + id, + direction, + source_chain, + destination_chain, + asset, + amount, + sender, + recipient, + status: TransferStatus::Pending, + source_tx_hash: None, + destination_tx_hash: None, + lock_block: None, + confirmations: 0, + required_confirmations, + created_at: current_time, + updated_at: current_time, + expires_at: 0, + error: None, + nonce, + } + } + + /// Create inbound transfer (external → Synor) + pub fn inbound( + source_chain: ChainType, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + required_confirmations: u64, + nonce: u64, + current_time: u64, + ) -> Self { + Self::new( + TransferDirection::Inbound, + source_chain, + ChainType::Synor, + asset, + amount, + sender, + recipient, + required_confirmations, + nonce, + current_time, + ) + } + + /// Create outbound transfer (Synor → external) + pub fn outbound( + destination_chain: ChainType, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + required_confirmations: u64, + nonce: u64, + current_time: u64, + ) -> Self { + Self::new( + TransferDirection::Outbound, + ChainType::Synor, + destination_chain, + asset, + amount, + sender, + recipient, + required_confirmations, + nonce, + current_time, + ) + } + + /// Set expiry + pub fn with_expiry(mut self, expires_at: u64) -> Self { + self.expires_at = expires_at; + self + } + + /// Update status + pub fn set_status(&mut self, status: TransferStatus, current_time: u64) { + self.status = status; + self.updated_at = current_time; + } + + /// Set lock confirmed + pub fn confirm_lock(&mut self, tx_hash: Vec, block_number: u64, current_time: u64) { + self.source_tx_hash = Some(tx_hash); + self.lock_block = Some(block_number); + self.status = TransferStatus::Locked; + self.updated_at = current_time; + } + + /// Update confirmations + pub fn update_confirmations(&mut self, confirmations: u64, current_time: u64) { + self.confirmations = confirmations; + self.updated_at = current_time; + + if confirmations >= self.required_confirmations && self.status == TransferStatus::Locked { + self.status = TransferStatus::Confirmed; + } + } + + /// Mark as minted + pub fn confirm_mint(&mut self, tx_hash: Vec, current_time: u64) { + self.destination_tx_hash = Some(tx_hash); + self.status = TransferStatus::Minted; + self.updated_at = current_time; + + // For inbound, minting is the final step + if self.direction == TransferDirection::Inbound { + self.status = TransferStatus::Completed; + } + } + + /// Mark as unlocked + pub fn confirm_unlock(&mut self, tx_hash: Vec, current_time: u64) { + self.destination_tx_hash = Some(tx_hash); + self.status = TransferStatus::Unlocked; + self.updated_at = current_time; + + // For outbound, unlocking is the final step + if self.direction == TransferDirection::Outbound { + self.status = TransferStatus::Completed; + } + } + + /// Mark as failed + pub fn fail(&mut self, error: impl Into, current_time: u64) { + self.error = Some(error.into()); + self.status = TransferStatus::Failed; + self.updated_at = current_time; + } + + /// Check if expired + pub fn is_expired(&self, current_time: u64) -> bool { + self.expires_at > 0 && current_time >= self.expires_at + } + + /// Check if transfer has sufficient confirmations + pub fn has_sufficient_confirmations(&self) -> bool { + self.confirmations >= self.required_confirmations + } + + /// Get completion percentage (0-100) + pub fn completion_percentage(&self) -> u8 { + match self.status { + TransferStatus::Pending => 0, + TransferStatus::Locked => 25, + TransferStatus::Confirmed => 50, + TransferStatus::Minted => 75, + TransferStatus::Unlocked => 90, + TransferStatus::Completed => 100, + TransferStatus::Failed | TransferStatus::Expired | TransferStatus::Refunded => 0, + } + } +} + +/// Transfer manager +pub struct TransferManager { + /// Transfers by ID + transfers: HashMap, + /// Transfers by sender + by_sender: HashMap>, + /// Transfers by recipient + by_recipient: HashMap>, + /// Pending transfers + pending: Vec, + /// Next nonce + next_nonce: u64, +} + +impl TransferManager { + /// Create a new transfer manager + pub fn new() -> Self { + Self { + transfers: HashMap::new(), + by_sender: HashMap::new(), + by_recipient: HashMap::new(), + pending: Vec::new(), + next_nonce: 0, + } + } + + /// Get next nonce + pub fn next_nonce(&mut self) -> u64 { + let nonce = self.next_nonce; + self.next_nonce += 1; + nonce + } + + /// Create a new inbound transfer + pub fn create_inbound( + &mut self, + source_chain: ChainType, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + required_confirmations: u64, + current_time: u64, + ) -> BridgeResult { + let nonce = self.next_nonce(); + let transfer = BridgeTransfer::inbound( + source_chain, + asset, + amount, + sender.clone(), + recipient.clone(), + required_confirmations, + nonce, + current_time, + ); + + self.register_transfer(transfer) + } + + /// Create a new outbound transfer + pub fn create_outbound( + &mut self, + destination_chain: ChainType, + asset: AssetId, + amount: u128, + sender: BridgeAddress, + recipient: BridgeAddress, + required_confirmations: u64, + current_time: u64, + ) -> BridgeResult { + let nonce = self.next_nonce(); + let transfer = BridgeTransfer::outbound( + destination_chain, + asset, + amount, + sender.clone(), + recipient.clone(), + required_confirmations, + nonce, + current_time, + ); + + self.register_transfer(transfer) + } + + /// Register a transfer + fn register_transfer(&mut self, transfer: BridgeTransfer) -> BridgeResult { + let id = transfer.id.clone(); + + if self.transfers.contains_key(&id) { + return Err(BridgeError::TransferAlreadyExists(id.to_string())); + } + + self.by_sender + .entry(transfer.sender.clone()) + .or_default() + .push(id.clone()); + + self.by_recipient + .entry(transfer.recipient.clone()) + .or_default() + .push(id.clone()); + + self.pending.push(id.clone()); + self.transfers.insert(id.clone(), transfer); + + Ok(id) + } + + /// Get transfer by ID + pub fn get(&self, id: &TransferId) -> Option<&BridgeTransfer> { + self.transfers.get(id) + } + + /// Get mutable transfer by ID + pub fn get_mut(&mut self, id: &TransferId) -> Option<&mut BridgeTransfer> { + self.transfers.get_mut(id) + } + + /// Confirm lock + pub fn confirm_lock( + &mut self, + id: &TransferId, + tx_hash: Vec, + block_number: u64, + current_time: u64, + ) -> BridgeResult<()> { + let transfer = self + .transfers + .get_mut(id) + .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?; + + transfer.confirm_lock(tx_hash, block_number, current_time); + Ok(()) + } + + /// Update confirmations + pub fn update_confirmations( + &mut self, + id: &TransferId, + confirmations: u64, + current_time: u64, + ) -> BridgeResult<()> { + let transfer = self + .transfers + .get_mut(id) + .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?; + + transfer.update_confirmations(confirmations, current_time); + + // Remove from pending if confirmed + if transfer.status == TransferStatus::Confirmed { + self.pending.retain(|pid| pid != id); + } + + Ok(()) + } + + /// Confirm mint + pub fn confirm_mint( + &mut self, + id: &TransferId, + tx_hash: Vec, + current_time: u64, + ) -> BridgeResult<()> { + let transfer = self + .transfers + .get_mut(id) + .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?; + + transfer.confirm_mint(tx_hash, current_time); + Ok(()) + } + + /// Confirm unlock + pub fn confirm_unlock( + &mut self, + id: &TransferId, + tx_hash: Vec, + current_time: u64, + ) -> BridgeResult<()> { + let transfer = self + .transfers + .get_mut(id) + .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?; + + transfer.confirm_unlock(tx_hash, current_time); + Ok(()) + } + + /// Mark transfer as failed + pub fn fail_transfer( + &mut self, + id: &TransferId, + error: impl Into, + current_time: u64, + ) -> BridgeResult<()> { + let transfer = self + .transfers + .get_mut(id) + .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?; + + transfer.fail(error, current_time); + self.pending.retain(|pid| pid != id); + + Ok(()) + } + + /// Get transfers by sender + pub fn by_sender(&self, sender: &BridgeAddress) -> Vec<&BridgeTransfer> { + self.by_sender + .get(sender) + .map(|ids| ids.iter().filter_map(|id| self.transfers.get(id)).collect()) + .unwrap_or_default() + } + + /// Get transfers by recipient + pub fn by_recipient(&self, recipient: &BridgeAddress) -> Vec<&BridgeTransfer> { + self.by_recipient + .get(recipient) + .map(|ids| ids.iter().filter_map(|id| self.transfers.get(id)).collect()) + .unwrap_or_default() + } + + /// Get pending transfers + pub fn pending_transfers(&self) -> Vec<&BridgeTransfer> { + self.pending + .iter() + .filter_map(|id| self.transfers.get(id)) + .collect() + } + + /// Get transfers ready for confirmation + pub fn ready_for_confirmation(&self) -> Vec<&BridgeTransfer> { + self.transfers + .values() + .filter(|t| t.status == TransferStatus::Confirmed) + .collect() + } + + /// Check and expire old transfers + pub fn expire_old_transfers(&mut self, current_time: u64) -> Vec { + let expired: Vec = self + .transfers + .iter() + .filter(|(_, t)| !t.status.is_finalized() && t.is_expired(current_time)) + .map(|(id, _)| id.clone()) + .collect(); + + for id in &expired { + if let Some(transfer) = self.transfers.get_mut(id) { + transfer.set_status(TransferStatus::Expired, current_time); + } + self.pending.retain(|pid| pid != id); + } + + expired + } + + /// Get transfer statistics + pub fn stats(&self) -> TransferStats { + let mut stats = TransferStats::default(); + + for transfer in self.transfers.values() { + stats.total_count += 1; + stats.total_volume += transfer.amount; + + match transfer.status { + TransferStatus::Pending | TransferStatus::Locked => stats.pending_count += 1, + TransferStatus::Confirmed | TransferStatus::Minted | TransferStatus::Unlocked => { + stats.in_progress_count += 1 + } + TransferStatus::Completed => { + stats.completed_count += 1; + stats.completed_volume += transfer.amount; + } + TransferStatus::Failed => stats.failed_count += 1, + TransferStatus::Expired | TransferStatus::Refunded => stats.expired_count += 1, + } + + match transfer.direction { + TransferDirection::Inbound => stats.inbound_count += 1, + TransferDirection::Outbound => stats.outbound_count += 1, + } + } + + stats + } +} + +impl Default for TransferManager { + fn default() -> Self { + Self::new() + } +} + +/// Transfer statistics +#[derive(Debug, Clone, Default)] +pub struct TransferStats { + pub total_count: u64, + pub total_volume: u128, + pub pending_count: u64, + pub in_progress_count: u64, + pub completed_count: u64, + pub completed_volume: u128, + pub failed_count: u64, + pub expired_count: u64, + pub inbound_count: u64, + pub outbound_count: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_sender() -> BridgeAddress { + BridgeAddress::from_eth([0xaa; 20]) + } + + fn test_recipient() -> BridgeAddress { + BridgeAddress::from_synor([0xbb; 32]) + } + + #[test] + fn test_transfer_id() { + let sender = test_sender(); + let recipient = test_recipient(); + let asset = AssetId::eth(); + + let id1 = TransferId::generate(&sender, &recipient, &asset, 1000, 0); + let id2 = TransferId::generate(&sender, &recipient, &asset, 1000, 0); + let id3 = TransferId::generate(&sender, &recipient, &asset, 1000, 1); + + assert_eq!(id1, id2); + assert_ne!(id1, id3); + } + + #[test] + fn test_transfer_lifecycle() { + let current_time = 1700000000; + let mut transfer = BridgeTransfer::inbound( + ChainType::Ethereum, + AssetId::eth(), + 1000, + test_sender(), + test_recipient(), + 12, + 0, + current_time, + ); + + assert_eq!(transfer.status, TransferStatus::Pending); + assert_eq!(transfer.completion_percentage(), 0); + + // Lock confirmed + transfer.confirm_lock(vec![0x11; 32], 100, current_time + 10); + assert_eq!(transfer.status, TransferStatus::Locked); + assert_eq!(transfer.completion_percentage(), 25); + + // Update confirmations + transfer.update_confirmations(6, current_time + 100); + assert_eq!(transfer.status, TransferStatus::Locked); + + transfer.update_confirmations(12, current_time + 200); + assert_eq!(transfer.status, TransferStatus::Confirmed); + assert_eq!(transfer.completion_percentage(), 50); + + // Mint confirmed + transfer.confirm_mint(vec![0x22; 32], current_time + 300); + assert_eq!(transfer.status, TransferStatus::Completed); + assert_eq!(transfer.completion_percentage(), 100); + } + + #[test] + fn test_transfer_manager() { + let mut manager = TransferManager::new(); + let current_time = 1700000000; + + let id = manager + .create_inbound( + ChainType::Ethereum, + AssetId::eth(), + 1000, + test_sender(), + test_recipient(), + 12, + current_time, + ) + .unwrap(); + + assert!(manager.get(&id).is_some()); + assert_eq!(manager.pending_transfers().len(), 1); + + // Confirm lock + manager.confirm_lock(&id, vec![0x11; 32], 100, current_time + 10).unwrap(); + + // Update confirmations + manager.update_confirmations(&id, 12, current_time + 100).unwrap(); + + // Should be ready for confirmation + assert_eq!(manager.ready_for_confirmation().len(), 1); + + // Confirm mint + manager.confirm_mint(&id, vec![0x22; 32], current_time + 200).unwrap(); + + let transfer = manager.get(&id).unwrap(); + assert_eq!(transfer.status, TransferStatus::Completed); + } + + #[test] + fn test_transfer_expiry() { + let mut manager = TransferManager::new(); + let current_time = 1700000000; + + let id = manager + .create_inbound( + ChainType::Ethereum, + AssetId::eth(), + 1000, + test_sender(), + test_recipient(), + 12, + current_time, + ) + .unwrap(); + + // Set expiry + if let Some(transfer) = manager.get_mut(&id) { + transfer.expires_at = current_time + 1000; + } + + // Not expired yet + let expired = manager.expire_old_transfers(current_time + 500); + assert!(expired.is_empty()); + + // Expired + let expired = manager.expire_old_transfers(current_time + 1500); + assert_eq!(expired.len(), 1); + + let transfer = manager.get(&id).unwrap(); + assert_eq!(transfer.status, TransferStatus::Expired); + } + + #[test] + fn test_transfer_stats() { + let mut manager = TransferManager::new(); + let current_time = 1700000000; + + // Create transfers + let id1 = manager + .create_inbound( + ChainType::Ethereum, + AssetId::eth(), + 1000, + test_sender(), + test_recipient(), + 12, + current_time, + ) + .unwrap(); + + let _id2 = manager + .create_outbound( + ChainType::Ethereum, + AssetId::eth(), + 500, + test_recipient(), + test_sender(), + 12, + current_time, + ) + .unwrap(); + + // Complete one + manager.confirm_lock(&id1, vec![0x11; 32], 100, current_time).unwrap(); + manager.update_confirmations(&id1, 12, current_time).unwrap(); + manager.confirm_mint(&id1, vec![0x22; 32], current_time).unwrap(); + + let stats = manager.stats(); + assert_eq!(stats.total_count, 2); + assert_eq!(stats.completed_count, 1); + assert_eq!(stats.pending_count, 1); + assert_eq!(stats.inbound_count, 1); + assert_eq!(stats.outbound_count, 1); + } +} diff --git a/crates/synor-bridge/src/vault.rs b/crates/synor-bridge/src/vault.rs new file mode 100644 index 0000000..128b915 --- /dev/null +++ b/crates/synor-bridge/src/vault.rs @@ -0,0 +1,519 @@ +//! Bridge Vault +//! +//! Manages locked assets for cross-chain transfers. +//! Assets are locked in vaults on the source chain and +//! wrapped tokens are minted on the destination chain. + +use crate::{AssetId, BridgeAddress, BridgeError, BridgeResult, ChainType}; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::fmt; + +/// Unique vault identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct VaultId(pub String); + +impl VaultId { + /// Create a new vault ID + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + /// Generate vault ID from asset + pub fn from_asset(asset: &AssetId, chain: &ChainType) -> Self { + let mut hasher = Sha256::new(); + hasher.update(chain.to_string().as_bytes()); + hasher.update(asset.identifier.as_bytes()); + Self(hex::encode(&hasher.finalize()[..16])) + } +} + +impl fmt::Display for VaultId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Locked asset record +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct LockedAsset { + /// Asset being locked + pub asset: AssetId, + /// Amount locked + pub amount: u128, + /// Who locked the asset + pub owner: BridgeAddress, + /// Recipient on destination chain + pub recipient: BridgeAddress, + /// Lock timestamp (Unix seconds) + pub locked_at: u64, + /// Lock expiry (for timelock, 0 = no expiry) + pub expires_at: u64, + /// Transaction hash on source chain + pub lock_tx_hash: Option>, + /// Whether the asset has been released + pub released: bool, +} + +impl LockedAsset { + /// Create a new locked asset record + pub fn new( + asset: AssetId, + amount: u128, + owner: BridgeAddress, + recipient: BridgeAddress, + locked_at: u64, + ) -> Self { + Self { + asset, + amount, + owner, + recipient, + locked_at, + expires_at: 0, + lock_tx_hash: None, + released: false, + } + } + + /// Set expiry time + pub fn with_expiry(mut self, expires_at: u64) -> Self { + self.expires_at = expires_at; + self + } + + /// Set lock transaction hash + pub fn with_tx_hash(mut self, tx_hash: Vec) -> Self { + self.lock_tx_hash = Some(tx_hash); + self + } + + /// Check if the lock has expired + pub fn is_expired(&self, current_time: u64) -> bool { + self.expires_at > 0 && current_time >= self.expires_at + } + + /// Mark as released + pub fn release(&mut self) { + self.released = true; + } +} + +/// Vault state +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum VaultState { + /// Vault is active and accepting deposits + Active, + /// Vault is paused (no new deposits) + Paused, + /// Vault is deprecated (migration needed) + Deprecated, +} + +/// Asset vault for a specific chain +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Vault { + /// Vault identifier + pub id: VaultId, + /// Chain this vault is on + pub chain: ChainType, + /// Asset managed by this vault + pub asset: AssetId, + /// Current vault state + pub state: VaultState, + /// Total locked amount + pub total_locked: u128, + /// Individual locked assets by lock ID + locked_assets: HashMap, + /// Vault address (contract address for EVM) + pub vault_address: Option, + /// Admin addresses + pub admins: Vec, + /// Daily limit (0 = unlimited) + pub daily_limit: u128, + /// Today's usage + daily_usage: u128, + /// Last reset timestamp + last_reset: u64, +} + +impl Vault { + /// Create a new vault + pub fn new(id: VaultId, chain: ChainType, asset: AssetId) -> Self { + Self { + id, + chain, + asset, + state: VaultState::Active, + total_locked: 0, + locked_assets: HashMap::new(), + vault_address: None, + admins: Vec::new(), + daily_limit: 0, + daily_usage: 0, + last_reset: 0, + } + } + + /// Set vault address + pub fn with_address(mut self, address: BridgeAddress) -> Self { + self.vault_address = Some(address); + self + } + + /// Set daily limit + pub fn with_daily_limit(mut self, limit: u128) -> Self { + self.daily_limit = limit; + self + } + + /// Add admin + pub fn add_admin(&mut self, admin: BridgeAddress) { + if !self.admins.contains(&admin) { + self.admins.push(admin); + } + } + + /// Lock assets in the vault + pub fn lock( + &mut self, + lock_id: impl Into, + amount: u128, + owner: BridgeAddress, + recipient: BridgeAddress, + current_time: u64, + ) -> BridgeResult<()> { + // Check vault state + if self.state != VaultState::Active { + return Err(BridgeError::BridgePaused); + } + + // Check daily limit + self.check_daily_limit(amount, current_time)?; + + let lock_id = lock_id.into(); + if self.locked_assets.contains_key(&lock_id) { + return Err(BridgeError::TransferAlreadyExists(lock_id)); + } + + let locked = LockedAsset::new( + self.asset.clone(), + amount, + owner, + recipient, + current_time, + ); + + self.locked_assets.insert(lock_id, locked); + self.total_locked += amount; + self.daily_usage += amount; + + Ok(()) + } + + /// Unlock assets from the vault + pub fn unlock(&mut self, lock_id: &str) -> BridgeResult { + let locked = self + .locked_assets + .get_mut(lock_id) + .ok_or_else(|| BridgeError::TransferNotFound(lock_id.to_string()))?; + + if locked.released { + return Err(BridgeError::TransferAlreadyCompleted(lock_id.to_string())); + } + + locked.release(); + self.total_locked = self.total_locked.saturating_sub(locked.amount); + + Ok(locked.clone()) + } + + /// Get locked asset + pub fn get_locked(&self, lock_id: &str) -> Option<&LockedAsset> { + self.locked_assets.get(lock_id) + } + + /// Check and update daily limit + fn check_daily_limit(&mut self, amount: u128, current_time: u64) -> BridgeResult<()> { + if self.daily_limit == 0 { + return Ok(()); + } + + // Reset daily usage if new day + let day = current_time / 86400; + let last_day = self.last_reset / 86400; + if day > last_day { + self.daily_usage = 0; + self.last_reset = current_time; + } + + // Check limit + if self.daily_usage + amount > self.daily_limit { + return Err(BridgeError::RateLimitExceeded); + } + + Ok(()) + } + + /// Pause the vault + pub fn pause(&mut self) { + self.state = VaultState::Paused; + } + + /// Resume the vault + pub fn resume(&mut self) { + self.state = VaultState::Active; + } + + /// Deprecate the vault + pub fn deprecate(&mut self) { + self.state = VaultState::Deprecated; + } + + /// Get all locked assets + pub fn all_locked(&self) -> impl Iterator { + self.locked_assets.iter() + } + + /// Get active (unreleased) locked assets + pub fn active_locked(&self) -> impl Iterator { + self.locked_assets.iter().filter(|(_, l)| !l.released) + } + + /// Get expired locked assets + pub fn expired_locked(&self, current_time: u64) -> impl Iterator { + self.locked_assets + .iter() + .filter(move |(_, l)| !l.released && l.is_expired(current_time)) + } +} + +/// Vault manager for multiple vaults +pub struct VaultManager { + /// Vaults by ID + vaults: HashMap, + /// Vault lookup by (chain, asset) + by_chain_asset: HashMap<(ChainType, String), VaultId>, +} + +impl VaultManager { + /// Create a new vault manager + pub fn new() -> Self { + Self { + vaults: HashMap::new(), + by_chain_asset: HashMap::new(), + } + } + + /// Create and register a new vault + pub fn create_vault(&mut self, chain: ChainType, asset: AssetId) -> VaultId { + let vault_id = VaultId::from_asset(&asset, &chain); + + let vault = Vault::new(vault_id.clone(), chain.clone(), asset.clone()); + + self.by_chain_asset + .insert((chain, asset.identifier.clone()), vault_id.clone()); + self.vaults.insert(vault_id.clone(), vault); + + vault_id + } + + /// Get vault by ID + pub fn get_vault(&self, vault_id: &VaultId) -> Option<&Vault> { + self.vaults.get(vault_id) + } + + /// Get mutable vault by ID + pub fn get_vault_mut(&mut self, vault_id: &VaultId) -> Option<&mut Vault> { + self.vaults.get_mut(vault_id) + } + + /// Find vault by chain and asset + pub fn find_vault(&self, chain: &ChainType, asset: &AssetId) -> Option<&Vault> { + self.by_chain_asset + .get(&(chain.clone(), asset.identifier.clone())) + .and_then(|id| self.vaults.get(id)) + } + + /// Find mutable vault by chain and asset + pub fn find_vault_mut(&mut self, chain: &ChainType, asset: &AssetId) -> Option<&mut Vault> { + let vault_id = self + .by_chain_asset + .get(&(chain.clone(), asset.identifier.clone()))? + .clone(); + self.vaults.get_mut(&vault_id) + } + + /// Get or create vault for asset + pub fn get_or_create_vault(&mut self, chain: ChainType, asset: AssetId) -> &mut Vault { + let key = (chain.clone(), asset.identifier.clone()); + if !self.by_chain_asset.contains_key(&key) { + self.create_vault(chain.clone(), asset.clone()); + } + let vault_id = self.by_chain_asset.get(&key).unwrap().clone(); + self.vaults.get_mut(&vault_id).unwrap() + } + + /// Get total locked value across all vaults + pub fn total_locked(&self) -> u128 { + self.vaults.values().map(|v| v.total_locked).sum() + } + + /// Get total locked value for an asset + pub fn total_locked_for_asset(&self, asset: &AssetId) -> u128 { + self.vaults + .values() + .filter(|v| v.asset.identifier == asset.identifier) + .map(|v| v.total_locked) + .sum() + } + + /// List all vault IDs + pub fn vault_ids(&self) -> Vec { + self.vaults.keys().cloned().collect() + } +} + +impl Default for VaultManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_owner() -> BridgeAddress { + BridgeAddress::from_eth([0xaa; 20]) + } + + fn test_recipient() -> BridgeAddress { + BridgeAddress::from_synor([0xbb; 32]) + } + + #[test] + fn test_vault_id() { + let asset = AssetId::eth(); + let id = VaultId::from_asset(&asset, &ChainType::Ethereum); + assert!(!id.0.is_empty()); + } + + #[test] + fn test_lock_unlock() { + let mut vault = Vault::new( + VaultId::new("test"), + ChainType::Ethereum, + AssetId::eth(), + ); + + let current_time = 1700000000; + + // Lock + vault + .lock("lock1", 1000, test_owner(), test_recipient(), current_time) + .unwrap(); + + assert_eq!(vault.total_locked, 1000); + assert!(vault.get_locked("lock1").is_some()); + + // Unlock + let released = vault.unlock("lock1").unwrap(); + assert_eq!(released.amount, 1000); + assert!(released.released); + assert_eq!(vault.total_locked, 0); + } + + #[test] + fn test_duplicate_lock() { + let mut vault = Vault::new( + VaultId::new("test"), + ChainType::Ethereum, + AssetId::eth(), + ); + + vault + .lock("lock1", 1000, test_owner(), test_recipient(), 0) + .unwrap(); + + // Duplicate should fail + let result = vault.lock("lock1", 500, test_owner(), test_recipient(), 0); + assert!(result.is_err()); + } + + #[test] + fn test_vault_pause() { + let mut vault = Vault::new( + VaultId::new("test"), + ChainType::Ethereum, + AssetId::eth(), + ); + + vault.pause(); + + let result = vault.lock("lock1", 1000, test_owner(), test_recipient(), 0); + assert!(matches!(result, Err(BridgeError::BridgePaused))); + } + + #[test] + fn test_daily_limit() { + let mut vault = Vault::new( + VaultId::new("test"), + ChainType::Ethereum, + AssetId::eth(), + ) + .with_daily_limit(1000); + + let current_time = 86400 * 100; // Day 100 + + // Under limit - OK + vault + .lock("lock1", 500, test_owner(), test_recipient(), current_time) + .unwrap(); + + // Exceed limit - fail + let result = vault.lock("lock2", 600, test_owner(), test_recipient(), current_time); + assert!(matches!(result, Err(BridgeError::RateLimitExceeded))); + + // Next day - reset + let next_day = current_time + 86400; + vault + .lock("lock2", 600, test_owner(), test_recipient(), next_day) + .unwrap(); + } + + #[test] + fn test_vault_manager() { + let mut manager = VaultManager::new(); + + let eth = AssetId::eth(); + let vault_id = manager.create_vault(ChainType::Ethereum, eth.clone()); + + assert!(manager.get_vault(&vault_id).is_some()); + assert!(manager.find_vault(&ChainType::Ethereum, ð).is_some()); + + // Get or create existing + let vault = manager.get_or_create_vault(ChainType::Ethereum, eth.clone()); + vault.lock("lock1", 100, test_owner(), test_recipient(), 0).unwrap(); + + assert_eq!(manager.total_locked(), 100); + } + + #[test] + fn test_locked_asset_expiry() { + let locked = LockedAsset::new( + AssetId::eth(), + 1000, + test_owner(), + test_recipient(), + 1000, + ) + .with_expiry(2000); + + assert!(!locked.is_expired(1500)); + assert!(locked.is_expired(2000)); + assert!(locked.is_expired(3000)); + } +} diff --git a/crates/synor-ibc/Cargo.toml b/crates/synor-ibc/Cargo.toml index f83185e..a9e0f5c 100644 --- a/crates/synor-ibc/Cargo.toml +++ b/crates/synor-ibc/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true description = "Inter-Blockchain Communication (IBC) protocol for Synor cross-chain interoperability" [dependencies] -# Local workspace crates synor-types = { path = "../synor-types" } synor-crypto = { path = "../synor-crypto" } @@ -18,9 +17,13 @@ borsh = { workspace = true } # Cryptography sha2 = "0.10" +sha3 = { workspace = true } blake3 = { workspace = true } rand = { workspace = true } +# Merkle proofs +rs_merkle = "1.4" + # Async runtime tokio = { version = "1", features = ["full"] } async-trait = "0.1" @@ -30,17 +33,13 @@ thiserror = { workspace = true } hex = { workspace = true } parking_lot = { workspace = true } tracing = "0.1" +chrono = { workspace = true } -# Protobuf (for IBC compatibility) -prost = "0.13" -prost-types = "0.13" - -# Time handling -chrono = { version = "0.4", features = ["serde"] } +# Protobuf for IBC messages (Cosmos compatibility) +prost = "0.12" +prost-types = "0.12" [dev-dependencies] -tempfile = { workspace = true } tokio-test = "0.4" - -[build-dependencies] -prost-build = "0.13" +proptest = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/synor-ibc/src/lib.rs b/crates/synor-ibc/src/lib.rs index 425cd7a..9b623dd 100644 --- a/crates/synor-ibc/src/lib.rs +++ b/crates/synor-ibc/src/lib.rs @@ -1,132 +1,241 @@ -//! Synor IBC - Inter-Blockchain Communication Protocol +//! Inter-Blockchain Communication (IBC) Protocol for Synor //! -//! This crate implements the IBC protocol for cross-chain communication, -//! enabling Synor to interoperate with Cosmos SDK chains and other IBC-compatible blockchains. +//! This crate implements the IBC protocol for cross-chain interoperability, +//! enabling Synor to communicate with Cosmos ecosystem chains and beyond. //! //! # Architecture //! //! ```text -//! ┌─────────────────────────────────────────────────────────────┐ -//! │ Synor Blockchain │ -//! ├─────────────────────────────────────────────────────────────┤ -//! │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ -//! │ │ IBC Client │ │ Connection │ │ Channel │ │ -//! │ │ (Light │ │ (Verified │ │ (Ordered/Unordered │ │ -//! │ │ Client) │ │ Link) │ │ Message Passing) │ │ -//! │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ -//! │ │ │ │ │ -//! │ └────────────────┼─────────────────────┘ │ -//! │ │ │ -//! │ ┌─────┴─────┐ │ -//! │ │ Packet │ │ -//! │ │ Handler │ │ -//! │ └─────┬─────┘ │ -//! └──────────────────────────┼──────────────────────────────────┘ -//! │ -//! ┌─────┴─────┐ -//! │ Relayer │ -//! └─────┬─────┘ -//! │ -//! ┌──────────────────────────┼──────────────────────────────────┐ -//! │ Remote Chain │ -//! └─────────────────────────────────────────────────────────────┘ +//! ┌─────────────────────────────────────────────────────────────────┐ +//! │ IBC Protocol Stack │ +//! ├─────────────────────────────────────────────────────────────────┤ +//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +//! │ │ Light Client │ │ Channel │ │ Connection │ │ +//! │ │ Verification │ │ Management │ │ Management │ │ +//! │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ +//! ├─────────────────────────────────────────────────────────────────┤ +//! │ ┌──────────────────────────────────────────────────────────┐ │ +//! │ │ Packet Relayer │ │ +//! │ │ - Packet commitment │ │ +//! │ │ - Acknowledgment handling │ │ +//! │ │ - Timeout management │ │ +//! │ └──────────────────────────────────────────────────────────┘ │ +//! ├─────────────────────────────────────────────────────────────────┤ +//! │ ┌──────────────────────────────────────────────────────────┐ │ +//! │ │ Atomic Swaps (HTLC) │ │ +//! │ │ - Hashlock verification │ │ +//! │ │ - Timelock expiration │ │ +//! │ │ - Cross-chain atomicity │ │ +//! │ └──────────────────────────────────────────────────────────┘ │ +//! └─────────────────────────────────────────────────────────────────┘ //! ``` //! //! # IBC Handshake Flow //! -//! ## Connection Handshake (4-way) -//! 1. `ConnOpenInit` - Chain A initiates -//! 2. `ConnOpenTry` - Chain B responds -//! 3. `ConnOpenAck` - Chain A acknowledges -//! 4. `ConnOpenConfirm` - Chain B confirms +//! ```text +//! Chain A Chain B +//! │ │ +//! ├──── ConnOpenInit ─────────────►│ +//! │ │ +//! │◄─── ConnOpenTry ───────────────┤ +//! │ │ +//! ├──── ConnOpenAck ─────────────►│ +//! │ │ +//! │◄─── ConnOpenConfirm ───────────┤ +//! │ │ +//! │ ═══ Connection Established ═══ │ +//! ``` //! -//! ## Channel Handshake (4-way) -//! 1. `ChanOpenInit` - Chain A initiates -//! 2. `ChanOpenTry` - Chain B responds -//! 3. `ChanOpenAck` - Chain A acknowledges -//! 4. `ChanOpenConfirm` - Chain B confirms +//! # Quick Start //! -//! # Example +//! ```rust,ignore +//! use synor_ibc::{IbcHandler, IbcConfig, ClientState, ConsensusState}; +//! use synor_ibc::{PortId, ChannelOrder, Timeout}; //! -//! ```ignore -//! use synor_ibc::{IbcHandler, ConnectionId, ChannelId}; +//! // Create IBC handler +//! let handler = IbcHandler::new(IbcConfig::default()); //! -//! // Initialize IBC handler -//! let handler = IbcHandler::new(config); +//! // Create a light client +//! let client_id = handler.create_client(client_state, consensus_state)?; //! -//! // Create a connection to a remote chain -//! let conn_id = handler.connection_open_init( -//! client_id, -//! counterparty, -//! version, -//! ).await?; +//! // Open a connection +//! let conn_id = handler.connection_open_init(client_id, counterparty_client_id, None)?; //! -//! // Open a channel for token transfers -//! let channel_id = handler.channel_open_init( -//! port_id, -//! conn_id, -//! counterparty_port, -//! version, -//! ).await?; +//! // Bind a port and open a channel +//! handler.bind_port(PortId::transfer(), "transfer".to_string())?; +//! let channel_id = handler.channel_open_init(port, conn_id, counterparty_port, ChannelOrder::Unordered, "ics20-1".to_string())?; //! //! // Send a packet -//! handler.send_packet( -//! channel_id, -//! data, -//! timeout, -//! ).await?; +//! let sequence = handler.send_packet(port, channel, data, Timeout::height(1000))?; //! ``` -pub mod client; -pub mod connection; +#![allow(dead_code)] + +// Core modules pub mod channel; -pub mod packet; +pub mod client; pub mod commitment; +pub mod connection; pub mod error; pub mod handler; +pub mod packet; pub mod swap; pub mod types; -pub use client::{ClientState, ConsensusState, LightClient}; -pub use connection::{ConnectionEnd, ConnectionState, ConnectionId, Counterparty}; -pub use channel::{Channel, ChannelState, ChannelId, PortId, ChannelOrder}; -pub use packet::{Packet, PacketCommitment, Acknowledgement, Timeout}; -pub use commitment::{CommitmentProof, MerkleProof, MerklePath}; +// Re-exports for convenience +pub use channel::{ + Channel, ChannelCounterparty, ChannelId, ChannelManager, ChannelOrder, ChannelState, PortId, +}; +pub use client::{ + ClientId, ClientState, ClientType, Commit, CommitSig, ConsensusState, Header, LightClient, + SignedHeader, TrustLevel, Validator, ValidatorSet, +}; +pub use commitment::{ + channel_path, client_state_path, connection_path, consensus_state_path, next_sequence_ack_path, + next_sequence_recv_path, next_sequence_send_path, packet_acknowledgement_path, + packet_commitment_path, packet_receipt_path, CommitmentProof, MerklePath, MerkleProof, ProofOp, + ProofOpType, +}; +pub use connection::{ + ConnectionEnd, ConnectionId, ConnectionManager, ConnectionState, + Counterparty as ConnectionCounterparty, +}; pub use error::{IbcError, IbcResult}; -pub use handler::IbcHandler; -pub use swap::{AtomicSwap, Htlc, SwapManager, SwapId, SwapState, SwapAsset, Hashlock, Timelock}; -pub use types::*; +pub use handler::{IbcConfig, IbcHandler}; +pub use packet::{ + Acknowledgement, FungibleTokenPacketData, Packet, PacketCommitment, PacketHandler, + PacketReceipt, Timeout, +}; +pub use swap::{ + AtomicSwap, Hashlock, Htlc, SwapAction, SwapAsset, SwapId, SwapManager, SwapPacketData, + SwapState, Timelock, +}; +pub use types::{ChainId, CommitmentPrefix, Height, Signer, Timestamp, Version}; /// IBC protocol version -pub const IBC_VERSION: &str = "1"; +pub const IBC_VERSION: &str = "1.0.0"; -/// Default packet timeout in blocks -pub const DEFAULT_TIMEOUT_HEIGHT: u64 = 1000; +/// Default timeout in blocks +pub const DEFAULT_TIMEOUT_BLOCKS: u64 = 1000; -/// Default packet timeout in nanoseconds (1 hour) -pub const DEFAULT_TIMEOUT_TIMESTAMP: u64 = 3_600_000_000_000; +/// Default timeout in nanoseconds (1 hour) +pub const DEFAULT_TIMEOUT_NANOS: u64 = 3_600_000_000_000; /// Maximum packet data size (1 MB) pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024; -/// Connection prefix for Synor -pub const SYNOR_CONNECTION_PREFIX: &str = "synor"; - -/// Well-known port for token transfers -pub const TRANSFER_PORT: &str = "transfer"; - -/// Well-known port for interchain accounts -pub const ICA_PORT: &str = "icahost"; +/// IBC module events +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum IbcEvent { + /// Client created + CreateClient { + client_id: ClientId, + client_type: ClientType, + consensus_height: Height, + }, + /// Client updated + UpdateClient { + client_id: ClientId, + consensus_height: Height, + }, + /// Connection opened (init) + OpenInitConnection { + connection_id: ConnectionId, + client_id: ClientId, + counterparty_client_id: ClientId, + }, + /// Connection opened (try) + OpenTryConnection { + connection_id: ConnectionId, + client_id: ClientId, + counterparty_connection_id: ConnectionId, + }, + /// Connection opened (ack) + OpenAckConnection { connection_id: ConnectionId }, + /// Connection opened (confirm) + OpenConfirmConnection { connection_id: ConnectionId }, + /// Channel opened (init) + OpenInitChannel { + port_id: PortId, + channel_id: ChannelId, + connection_id: ConnectionId, + }, + /// Channel opened (try) + OpenTryChannel { + port_id: PortId, + channel_id: ChannelId, + counterparty_channel_id: ChannelId, + }, + /// Channel opened (ack) + OpenAckChannel { + port_id: PortId, + channel_id: ChannelId, + }, + /// Channel opened (confirm) + OpenConfirmChannel { + port_id: PortId, + channel_id: ChannelId, + }, + /// Channel closed + CloseChannel { + port_id: PortId, + channel_id: ChannelId, + }, + /// Packet sent + SendPacket { packet: Packet }, + /// Packet received + ReceivePacket { packet: Packet }, + /// Packet acknowledged + AcknowledgePacket { + packet: Packet, + acknowledgement: Acknowledgement, + }, + /// Packet timed out + TimeoutPacket { packet: Packet }, + /// Swap initiated + SwapInitiated { + swap_id: SwapId, + initiator: String, + responder: String, + }, + /// Swap completed + SwapCompleted { swap_id: SwapId }, + /// Swap refunded + SwapRefunded { swap_id: SwapId }, +} #[cfg(test)] mod tests { use super::*; #[test] - fn test_constants() { - assert_eq!(IBC_VERSION, "1"); - assert!(DEFAULT_TIMEOUT_HEIGHT > 0); - assert!(DEFAULT_TIMEOUT_TIMESTAMP > 0); - assert!(MAX_PACKET_DATA_SIZE > 0); + fn test_chain_id() { + let chain = ChainId::new("synor-1"); + assert_eq!(chain.to_string(), "synor-1"); + } + + #[test] + fn test_commitment_prefix() { + let prefix = CommitmentPrefix::default(); + assert_eq!(prefix.key_prefix, b"ibc"); + } + + #[test] + fn test_height() { + let h = Height::new(1, 100); + assert_eq!(h.revision_number, 1); + assert_eq!(h.revision_height, 100); + } + + #[test] + fn test_ibc_handler_creation() { + let handler = IbcHandler::default(); + assert_eq!(handler.chain_id(), "synor-1"); + } + + #[test] + fn test_swap_manager_creation() { + let manager = SwapManager::new(); + assert!(manager.get_active_swaps().is_empty()); } }