feat(bridge): add synor-bridge crate for cross-chain interoperability

Phase 14: Interoperability & Privacy enhancements

New synor-bridge crate with Ethereum lock-mint bridge:
- Bridge trait for generic cross-chain implementations
- Vault management with daily limits and pause controls
- Transfer lifecycle (pending → confirmed → minted)
- Multi-relayer signature verification
- Wrapped token minting (ETH → sETH, ERC20 → sERC20)
- Burn-unlock flow for redemption

Also fixes synor-ibc lib.rs exports and adds rand dependency.

21 tests passing for synor-bridge.
This commit is contained in:
Gulshan Yadav 2026-01-20 01:38:37 +05:30
parent af79e21a1b
commit 45ccbcba03
8 changed files with 2845 additions and 100 deletions

View file

@ -16,6 +16,7 @@ members = [
"crates/synor-mining", "crates/synor-mining",
"crates/synor-zk", "crates/synor-zk",
"crates/synor-ibc", "crates/synor-ibc",
"crates/synor-bridge",
"crates/synor-privacy", "crates/synor-privacy",
"crates/synor-sharding", "crates/synor-sharding",
"crates/synor-verifier", "crates/synor-verifier",

View file

@ -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 }

View file

@ -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<Address, AssetId>,
/// Relayer addresses (for multi-sig)
pub relayers: Vec<Address>,
/// Required relayer signatures
pub required_signatures: usize,
/// Whether bridge is paused
pub paused: bool,
/// Daily limit per token (address → limit)
pub daily_limits: HashMap<Address, U256>,
}
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<u8>,
/// 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<u8>,
}
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<EthereumBridgeConfig>,
/// Transfer manager
transfers: Arc<RwLock<TransferManager>>,
/// Vault manager
vaults: Arc<RwLock<VaultManager>>,
/// Wrapped tokens by original address
wrapped_tokens: RwLock<HashMap<Address, WrappedToken>>,
/// Pending events awaiting confirmation
pending_events: RwLock<HashMap<B256, EthereumEvent>>,
/// Processed event hashes (to prevent replay)
processed_events: RwLock<HashMap<B256, bool>>,
/// Relayer signatures per event
relayer_signatures: RwLock<HashMap<B256, Vec<(Address, Vec<u8>)>>>,
}
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<F>(&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<TransferId> {
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::<u128>();
// 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<u8>,
) -> BridgeResult<bool> {
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<Vec<TransferId>> {
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<TransferId> {
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<WrappedToken> {
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<RwLock<TransferManager>> {
self.transfers.clone()
}
/// Get vault manager
pub fn vault_manager(&self) -> Arc<RwLock<VaultManager>> {
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<TransferId> {
// 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<bool> {
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<TransferId> {
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<TransferStatus> {
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());
}
}

View file

@ -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<u64> {
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<String>, symbol: impl Into<String>, 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<u8>,
}
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<Self, BridgeError> {
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<T> = std::result::Result<T, BridgeError>;
/// 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<TransferId>;
/// Verify a lock proof
async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult<bool>;
/// 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<TransferId>;
/// Unlock original tokens
async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()>;
/// Get transfer status
async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult<TransferStatus>;
}
/// 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(&eth);
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]);
}
}

View file

@ -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<String>) -> 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<Vec<u8>>,
/// Destination chain transaction hash
pub destination_tx_hash: Option<Vec<u8>>,
/// Block number where lock occurred
pub lock_block: Option<u64>,
/// 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<String>,
/// 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<u8>, 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<u8>, 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<u8>, 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<String>, 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<TransferId, BridgeTransfer>,
/// Transfers by sender
by_sender: HashMap<BridgeAddress, Vec<TransferId>>,
/// Transfers by recipient
by_recipient: HashMap<BridgeAddress, Vec<TransferId>>,
/// Pending transfers
pending: Vec<TransferId>,
/// 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<TransferId> {
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<TransferId> {
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<TransferId> {
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<u8>,
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<u8>,
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<u8>,
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<String>,
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<TransferId> {
let expired: Vec<TransferId> = 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);
}
}

View file

@ -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<String>) -> 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<Vec<u8>>,
/// 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<u8>) -> 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<String, LockedAsset>,
/// Vault address (contract address for EVM)
pub vault_address: Option<BridgeAddress>,
/// Admin addresses
pub admins: Vec<BridgeAddress>,
/// 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<String>,
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<LockedAsset> {
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<Item = (&String, &LockedAsset)> {
self.locked_assets.iter()
}
/// Get active (unreleased) locked assets
pub fn active_locked(&self) -> impl Iterator<Item = (&String, &LockedAsset)> {
self.locked_assets.iter().filter(|(_, l)| !l.released)
}
/// Get expired locked assets
pub fn expired_locked(&self, current_time: u64) -> impl Iterator<Item = (&String, &LockedAsset)> {
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<VaultId, Vault>,
/// 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<VaultId> {
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, &eth).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));
}
}

View file

@ -7,7 +7,6 @@ license.workspace = true
description = "Inter-Blockchain Communication (IBC) protocol for Synor cross-chain interoperability" description = "Inter-Blockchain Communication (IBC) protocol for Synor cross-chain interoperability"
[dependencies] [dependencies]
# Local workspace crates
synor-types = { path = "../synor-types" } synor-types = { path = "../synor-types" }
synor-crypto = { path = "../synor-crypto" } synor-crypto = { path = "../synor-crypto" }
@ -18,9 +17,13 @@ borsh = { workspace = true }
# Cryptography # Cryptography
sha2 = "0.10" sha2 = "0.10"
sha3 = { workspace = true }
blake3 = { workspace = true } blake3 = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
# Merkle proofs
rs_merkle = "1.4"
# Async runtime # Async runtime
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
async-trait = "0.1" async-trait = "0.1"
@ -30,17 +33,13 @@ thiserror = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
tracing = "0.1" tracing = "0.1"
chrono = { workspace = true }
# Protobuf (for IBC compatibility) # Protobuf for IBC messages (Cosmos compatibility)
prost = "0.13" prost = "0.12"
prost-types = "0.13" prost-types = "0.12"
# Time handling
chrono = { version = "0.4", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true }
tokio-test = "0.4" tokio-test = "0.4"
proptest = { workspace = true }
[build-dependencies] tempfile = { workspace = true }
prost-build = "0.13"

View file

@ -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, //! This crate implements the IBC protocol for cross-chain interoperability,
//! enabling Synor to interoperate with Cosmos SDK chains and other IBC-compatible blockchains. //! enabling Synor to communicate with Cosmos ecosystem chains and beyond.
//! //!
//! # Architecture //! # Architecture
//! //!
//! ```text //! ```text
//! ┌─────────────────────────────────────────────────────────────┐ //! ┌─────────────────────────────────────────────────────────────────┐
//! │ Synor Blockchain │ //! │ IBC Protocol Stack │
//! ├─────────────────────────────────────────────────────────────┤ //! ├─────────────────────────────────────────────────────────────────┤
//! │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ //! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
//! │ │ IBC Client │ │ Connection │ │ Channel │ │ //! │ │ Light Client │ │ Channel │ │ Connection │ │
//! │ │ (Light │ │ (Verified │ │ (Ordered/Unordered │ │ //! │ │ Verification │ │ Management │ │ Management │ │
//! │ │ Client) │ │ Link) │ │ Message Passing) │ │ //! │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
//! │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ //! ├─────────────────────────────────────────────────────────────────┤
//! │ │ │ │ │ //! │ ┌──────────────────────────────────────────────────────────┐ │
//! │ └────────────────┼─────────────────────┘ │ //! │ │ Packet Relayer │ │
//! │ │ │ //! │ │ - Packet commitment │ │
//! │ ┌─────┴─────┐ │ //! │ │ - Acknowledgment handling │ │
//! │ │ Packet │ │ //! │ │ - Timeout management │ │
//! │ │ Handler │ │ //! │ └──────────────────────────────────────────────────────────┘ │
//! │ └─────┬─────┘ │ //! ├─────────────────────────────────────────────────────────────────┤
//! └──────────────────────────┼──────────────────────────────────┘ //! │ ┌──────────────────────────────────────────────────────────┐ │
//! │ //! │ │ Atomic Swaps (HTLC) │ │
//! ┌─────┴─────┐ //! │ │ - Hashlock verification │ │
//! │ Relayer │ //! │ │ - Timelock expiration │ │
//! └─────┬─────┘ //! │ │ - Cross-chain atomicity │ │
//! │ //! │ └──────────────────────────────────────────────────────────┘ │
//! ┌──────────────────────────┼──────────────────────────────────┐ //! └─────────────────────────────────────────────────────────────────┘
//! │ Remote Chain │
//! └─────────────────────────────────────────────────────────────┘
//! ``` //! ```
//! //!
//! # IBC Handshake Flow //! # IBC Handshake Flow
//! //!
//! ## Connection Handshake (4-way) //! ```text
//! 1. `ConnOpenInit` - Chain A initiates //! Chain A Chain B
//! 2. `ConnOpenTry` - Chain B responds //! │ │
//! 3. `ConnOpenAck` - Chain A acknowledges //! ├──── ConnOpenInit ─────────────►│
//! 4. `ConnOpenConfirm` - Chain B confirms //! │ │
//! │◄─── ConnOpenTry ───────────────┤
//! │ │
//! ├──── ConnOpenAck ─────────────►│
//! │ │
//! │◄─── ConnOpenConfirm ───────────┤
//! │ │
//! │ ═══ Connection Established ═══ │
//! ```
//! //!
//! ## Channel Handshake (4-way) //! # Quick Start
//! 1. `ChanOpenInit` - Chain A initiates
//! 2. `ChanOpenTry` - Chain B responds
//! 3. `ChanOpenAck` - Chain A acknowledges
//! 4. `ChanOpenConfirm` - Chain B confirms
//! //!
//! # Example //! ```rust,ignore
//! use synor_ibc::{IbcHandler, IbcConfig, ClientState, ConsensusState};
//! use synor_ibc::{PortId, ChannelOrder, Timeout};
//! //!
//! ```ignore //! // Create IBC handler
//! use synor_ibc::{IbcHandler, ConnectionId, ChannelId}; //! let handler = IbcHandler::new(IbcConfig::default());
//! //!
//! // Initialize IBC handler //! // Create a light client
//! let handler = IbcHandler::new(config); //! let client_id = handler.create_client(client_state, consensus_state)?;
//! //!
//! // Create a connection to a remote chain //! // Open a connection
//! let conn_id = handler.connection_open_init( //! let conn_id = handler.connection_open_init(client_id, counterparty_client_id, None)?;
//! client_id,
//! counterparty,
//! version,
//! ).await?;
//! //!
//! // Open a channel for token transfers //! // Bind a port and open a channel
//! let channel_id = handler.channel_open_init( //! handler.bind_port(PortId::transfer(), "transfer".to_string())?;
//! port_id, //! let channel_id = handler.channel_open_init(port, conn_id, counterparty_port, ChannelOrder::Unordered, "ics20-1".to_string())?;
//! conn_id,
//! counterparty_port,
//! version,
//! ).await?;
//! //!
//! // Send a packet //! // Send a packet
//! handler.send_packet( //! let sequence = handler.send_packet(port, channel, data, Timeout::height(1000))?;
//! channel_id,
//! data,
//! timeout,
//! ).await?;
//! ``` //! ```
pub mod client; #![allow(dead_code)]
pub mod connection;
// Core modules
pub mod channel; pub mod channel;
pub mod packet; pub mod client;
pub mod commitment; pub mod commitment;
pub mod connection;
pub mod error; pub mod error;
pub mod handler; pub mod handler;
pub mod packet;
pub mod swap; pub mod swap;
pub mod types; pub mod types;
pub use client::{ClientState, ConsensusState, LightClient}; // Re-exports for convenience
pub use connection::{ConnectionEnd, ConnectionState, ConnectionId, Counterparty}; pub use channel::{
pub use channel::{Channel, ChannelState, ChannelId, PortId, ChannelOrder}; Channel, ChannelCounterparty, ChannelId, ChannelManager, ChannelOrder, ChannelState, PortId,
pub use packet::{Packet, PacketCommitment, Acknowledgement, Timeout}; };
pub use commitment::{CommitmentProof, MerkleProof, MerklePath}; 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 error::{IbcError, IbcResult};
pub use handler::IbcHandler; pub use handler::{IbcConfig, IbcHandler};
pub use swap::{AtomicSwap, Htlc, SwapManager, SwapId, SwapState, SwapAsset, Hashlock, Timelock}; pub use packet::{
pub use types::*; 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 /// IBC protocol version
pub const IBC_VERSION: &str = "1"; pub const IBC_VERSION: &str = "1.0.0";
/// Default packet timeout in blocks /// Default timeout in blocks
pub const DEFAULT_TIMEOUT_HEIGHT: u64 = 1000; pub const DEFAULT_TIMEOUT_BLOCKS: u64 = 1000;
/// Default packet timeout in nanoseconds (1 hour) /// Default timeout in nanoseconds (1 hour)
pub const DEFAULT_TIMEOUT_TIMESTAMP: u64 = 3_600_000_000_000; pub const DEFAULT_TIMEOUT_NANOS: u64 = 3_600_000_000_000;
/// Maximum packet data size (1 MB) /// Maximum packet data size (1 MB)
pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024; pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024;
/// Connection prefix for Synor /// IBC module events
pub const SYNOR_CONNECTION_PREFIX: &str = "synor"; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum IbcEvent {
/// Well-known port for token transfers /// Client created
pub const TRANSFER_PORT: &str = "transfer"; CreateClient {
client_id: ClientId,
/// Well-known port for interchain accounts client_type: ClientType,
pub const ICA_PORT: &str = "icahost"; 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_constants() { fn test_chain_id() {
assert_eq!(IBC_VERSION, "1"); let chain = ChainId::new("synor-1");
assert!(DEFAULT_TIMEOUT_HEIGHT > 0); assert_eq!(chain.to_string(), "synor-1");
assert!(DEFAULT_TIMEOUT_TIMESTAMP > 0); }
assert!(MAX_PACKET_DATA_SIZE > 0);
#[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());
} }
} }