feat(ibc): add Phase 14 Milestone 1 - Cross-Chain IBC Interoperability
Implements full Inter-Blockchain Communication (IBC) protocol: synor-ibc crate (new): - Light client management (create, update, verify headers) - Connection handshake (4-way: Init, Try, Ack, Confirm) - Channel handshake (4-way: Init, Try, Ack, Confirm) - Packet handling (send, receive, acknowledge, timeout) - Merkle commitment proofs for state verification - ICS-20 fungible token transfer support - Atomic swap engine with HTLC (hashlock + timelock) IBC Bridge Contract (contracts/ibc-bridge): - Token locking/unlocking for cross-chain transfers - Relayer whitelist management - Channel registration and sequence tracking - HTLC atomic swap (create, claim, refund) - Event emission for indexing - 52KB optimized WASM binary Test coverage: 40 tests passing
This commit is contained in:
parent
d73909d72c
commit
6037695afb
14 changed files with 6219 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ members = [
|
||||||
"crates/synor-vm",
|
"crates/synor-vm",
|
||||||
"crates/synor-mining",
|
"crates/synor-mining",
|
||||||
"crates/synor-zk",
|
"crates/synor-zk",
|
||||||
|
"crates/synor-ibc",
|
||||||
"crates/synor-sdk",
|
"crates/synor-sdk",
|
||||||
"crates/synor-contract-test",
|
"crates/synor-contract-test",
|
||||||
"crates/synor-compiler",
|
"crates/synor-compiler",
|
||||||
|
|
|
||||||
25
contracts/ibc-bridge/Cargo.toml
Normal file
25
contracts/ibc-bridge/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "synor-ibc-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "IBC Bridge Contract for cross-chain token transfers and atomic swaps"
|
||||||
|
authors = ["Synor Team <team@synor.cc>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
# Exclude from parent workspace - contracts are standalone WASM builds
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
synor-sdk = { path = "../../crates/synor-sdk", default-features = false }
|
||||||
|
borsh = { version = "1.3", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z" # Optimize for size
|
||||||
|
lto = true # Link-time optimization
|
||||||
|
codegen-units = 1 # Single codegen unit for better optimization
|
||||||
|
panic = "abort" # Abort on panic (smaller binaries)
|
||||||
|
strip = true # Strip symbols
|
||||||
944
contracts/ibc-bridge/src/lib.rs
Normal file
944
contracts/ibc-bridge/src/lib.rs
Normal file
|
|
@ -0,0 +1,944 @@
|
||||||
|
//! Synor IBC Bridge Contract
|
||||||
|
//!
|
||||||
|
//! This contract enables cross-chain token transfers and atomic swaps via IBC.
|
||||||
|
//!
|
||||||
|
//! # Features
|
||||||
|
//!
|
||||||
|
//! - **ICS-20 Token Transfers**: Lock/unlock native tokens for cross-chain transfers
|
||||||
|
//! - **Atomic Swaps**: HTLC-based trustless token exchanges
|
||||||
|
//! - **Light Client Verification**: Validates IBC proofs from counterparty chains
|
||||||
|
//! - **Channel Management**: Manages IBC channels and sequences
|
||||||
|
//!
|
||||||
|
//! # Security
|
||||||
|
//!
|
||||||
|
//! - Only whitelisted relayers can submit IBC messages
|
||||||
|
//! - Merkle proof verification for all cross-chain state
|
||||||
|
//! - Timeout handling prevents stuck funds
|
||||||
|
//!
|
||||||
|
//! # Methods
|
||||||
|
//!
|
||||||
|
//! ## Admin
|
||||||
|
//! - `init(admin, chain_id)` - Initialize the bridge
|
||||||
|
//! - `add_relayer(address)` - Whitelist a relayer
|
||||||
|
//! - `remove_relayer(address)` - Remove a relayer
|
||||||
|
//! - `register_channel(channel_id, counterparty)` - Register IBC channel
|
||||||
|
//!
|
||||||
|
//! ## ICS-20 Transfers
|
||||||
|
//! - `lock_tokens(amount, receiver, channel)` - Lock tokens for transfer
|
||||||
|
//! - `unlock_tokens(packet, proof)` - Unlock tokens from incoming transfer
|
||||||
|
//! - `timeout_packet(packet)` - Handle packet timeout (refund)
|
||||||
|
//!
|
||||||
|
//! ## Atomic Swaps
|
||||||
|
//! - `create_swap(hashlock, receiver, amount, timelock)` - Create HTLC
|
||||||
|
//! - `claim_swap(swap_id, secret)` - Claim with preimage
|
||||||
|
//! - `refund_swap(swap_id)` - Refund after timeout
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use synor_sdk::prelude::*;
|
||||||
|
use synor_sdk::{require, require_auth};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STORAGE KEYS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
mod keys {
|
||||||
|
/// Contract admin address
|
||||||
|
pub const ADMIN: &[u8] = b"ibc:admin";
|
||||||
|
/// Whether the contract is initialized
|
||||||
|
pub const INITIALIZED: &[u8] = b"ibc:init";
|
||||||
|
/// Chain ID for this bridge
|
||||||
|
pub const CHAIN_ID: &[u8] = b"ibc:chain_id";
|
||||||
|
/// Whitelisted relayers prefix
|
||||||
|
pub const RELAYERS: &[u8] = b"ibc:relayers";
|
||||||
|
/// Registered channels prefix
|
||||||
|
pub const CHANNELS: &[u8] = b"ibc:channels";
|
||||||
|
/// Packet commitments prefix
|
||||||
|
pub const COMMITMENTS: &[u8] = b"ibc:commits";
|
||||||
|
/// Packet receipts prefix
|
||||||
|
pub const RECEIPTS: &[u8] = b"ibc:receipts";
|
||||||
|
/// Packet acknowledgements prefix
|
||||||
|
pub const ACKS: &[u8] = b"ibc:acks";
|
||||||
|
/// Sequence counters prefix
|
||||||
|
pub const SEQUENCES: &[u8] = b"ibc:seq";
|
||||||
|
/// Locked tokens total
|
||||||
|
pub const LOCKED_TOTAL: &[u8] = b"ibc:locked";
|
||||||
|
/// Active swaps prefix
|
||||||
|
pub const SWAPS: &[u8] = b"ibc:swaps";
|
||||||
|
/// Swap counter for IDs
|
||||||
|
pub const SWAP_COUNTER: &[u8] = b"ibc:swap_cnt";
|
||||||
|
/// Token escrow per user
|
||||||
|
pub const ESCROW: &[u8] = b"ibc:escrow";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DATA STRUCTURES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// IBC Channel configuration
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct ChannelConfig {
|
||||||
|
/// Channel ID on this chain
|
||||||
|
pub channel_id: String,
|
||||||
|
/// Counterparty channel ID
|
||||||
|
pub counterparty_channel: String,
|
||||||
|
/// Counterparty chain ID
|
||||||
|
pub counterparty_chain: String,
|
||||||
|
/// Port ID (usually "transfer")
|
||||||
|
pub port_id: String,
|
||||||
|
/// Whether channel is active
|
||||||
|
pub active: bool,
|
||||||
|
/// Next send sequence
|
||||||
|
pub next_send_seq: u64,
|
||||||
|
/// Next receive sequence
|
||||||
|
pub next_recv_seq: u64,
|
||||||
|
/// Next acknowledge sequence
|
||||||
|
pub next_ack_seq: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelConfig {
|
||||||
|
pub fn new(
|
||||||
|
channel_id: String,
|
||||||
|
counterparty_channel: String,
|
||||||
|
counterparty_chain: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
channel_id,
|
||||||
|
counterparty_channel,
|
||||||
|
counterparty_chain,
|
||||||
|
port_id: String::from("transfer"),
|
||||||
|
active: true,
|
||||||
|
next_send_seq: 1,
|
||||||
|
next_recv_seq: 1,
|
||||||
|
next_ack_seq: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IBC Packet data for token transfers (ICS-20)
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct TokenPacket {
|
||||||
|
/// Token denomination
|
||||||
|
pub denom: String,
|
||||||
|
/// Amount to transfer
|
||||||
|
pub amount: u128,
|
||||||
|
/// Sender on source chain
|
||||||
|
pub sender: Address,
|
||||||
|
/// Receiver on destination chain (as string for cross-chain)
|
||||||
|
pub receiver: String,
|
||||||
|
/// Memo field
|
||||||
|
pub memo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet commitment stored for verification
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct PacketCommitment {
|
||||||
|
/// Sequence number
|
||||||
|
pub sequence: u64,
|
||||||
|
/// Packet data hash
|
||||||
|
pub data_hash: [u8; 32],
|
||||||
|
/// Timeout height
|
||||||
|
pub timeout_height: u64,
|
||||||
|
/// Timeout timestamp
|
||||||
|
pub timeout_timestamp: u64,
|
||||||
|
/// Source channel
|
||||||
|
pub source_channel: String,
|
||||||
|
/// Destination channel
|
||||||
|
pub dest_channel: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTLC Swap state
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
|
||||||
|
pub enum SwapState {
|
||||||
|
/// Swap is pending/locked
|
||||||
|
Active,
|
||||||
|
/// Swap completed (claimed)
|
||||||
|
Completed,
|
||||||
|
/// Swap refunded
|
||||||
|
Refunded,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashed Time-Locked Contract for atomic swaps
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct Htlc {
|
||||||
|
/// Unique swap ID
|
||||||
|
pub id: u64,
|
||||||
|
/// Creator/sender
|
||||||
|
pub sender: Address,
|
||||||
|
/// Recipient
|
||||||
|
pub receiver: Address,
|
||||||
|
/// Amount locked
|
||||||
|
pub amount: u128,
|
||||||
|
/// SHA256 hash of the secret
|
||||||
|
pub hashlock: [u8; 32],
|
||||||
|
/// Expiration timestamp
|
||||||
|
pub timelock: u64,
|
||||||
|
/// Current state
|
||||||
|
pub state: SwapState,
|
||||||
|
/// Secret (only set after claim)
|
||||||
|
pub secret: Option<[u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Htlc {
|
||||||
|
pub fn is_expired(&self, current_time: u64) -> bool {
|
||||||
|
current_time >= self.timelock
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_claim(&self, current_time: u64) -> bool {
|
||||||
|
self.state == SwapState::Active && !self.is_expired(current_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_refund(&self, current_time: u64) -> bool {
|
||||||
|
self.state == SwapState::Active && self.is_expired(current_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// EVENTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Event topic hashes (first 32 bytes of blake3 hash of event name)
|
||||||
|
mod event_topics {
|
||||||
|
pub const TOKENS_LOCKED: [u8; 32] = *b"IBC:TokensLocked________________";
|
||||||
|
pub const TOKENS_UNLOCKED: [u8; 32] = *b"IBC:TokensUnlocked______________";
|
||||||
|
pub const PACKET_TIMEOUT: [u8; 32] = *b"IBC:PacketTimeout_______________";
|
||||||
|
pub const SWAP_CREATED: [u8; 32] = *b"IBC:SwapCreated_________________";
|
||||||
|
pub const SWAP_CLAIMED: [u8; 32] = *b"IBC:SwapClaimed_________________";
|
||||||
|
pub const SWAP_REFUNDED: [u8; 32] = *b"IBC:SwapRefunded________________";
|
||||||
|
pub const CHANNEL_REGISTERED: [u8; 32] = *b"IBC:ChannelRegistered___________";
|
||||||
|
pub const RELAYER_ADDED: [u8; 32] = *b"IBC:RelayerAdded________________";
|
||||||
|
pub const RELAYER_REMOVED: [u8; 32] = *b"IBC:RelayerRemoved______________";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct TokensLockedData {
|
||||||
|
pub sender: Address,
|
||||||
|
pub receiver: String,
|
||||||
|
pub amount: u128,
|
||||||
|
pub channel: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct TokensUnlockedData {
|
||||||
|
pub receiver: Address,
|
||||||
|
pub amount: u128,
|
||||||
|
pub source_channel: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct PacketTimeoutData {
|
||||||
|
pub sender: Address,
|
||||||
|
pub amount: u128,
|
||||||
|
pub channel: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct SwapCreatedData {
|
||||||
|
pub id: u64,
|
||||||
|
pub sender: Address,
|
||||||
|
pub receiver: Address,
|
||||||
|
pub amount: u128,
|
||||||
|
pub hashlock: [u8; 32],
|
||||||
|
pub timelock: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct SwapClaimedData {
|
||||||
|
pub id: u64,
|
||||||
|
pub claimer: Address,
|
||||||
|
pub secret: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct SwapRefundedData {
|
||||||
|
pub id: u64,
|
||||||
|
pub sender: Address,
|
||||||
|
pub amount: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct ChannelRegisteredData {
|
||||||
|
pub channel_id: String,
|
||||||
|
pub counterparty_chain: String,
|
||||||
|
pub counterparty_channel: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct RelayerAddedData {
|
||||||
|
pub relayer: Address,
|
||||||
|
pub added_by: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
pub struct RelayerRemovedData {
|
||||||
|
pub relayer: Address,
|
||||||
|
pub removed_by: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STORAGE HELPERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn get_admin() -> Option<Address> {
|
||||||
|
storage::get::<Address>(keys::ADMIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_admin(addr: &Address) -> bool {
|
||||||
|
get_admin().map(|a| a == *addr).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized() -> bool {
|
||||||
|
storage::get::<bool>(keys::INITIALIZED).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_relayer(addr: &Address) -> bool {
|
||||||
|
storage::get_with_suffix::<bool>(keys::RELAYERS, addr.as_bytes())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(channel_id: &str) -> Option<ChannelConfig> {
|
||||||
|
storage::get_with_suffix::<ChannelConfig>(keys::CHANNELS, channel_id.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_channel(config: &ChannelConfig) {
|
||||||
|
storage::set_with_suffix(keys::CHANNELS, config.channel_id.as_bytes(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_swap(id: u64) -> Option<Htlc> {
|
||||||
|
storage::get_with_suffix::<Htlc>(keys::SWAPS, &id.to_le_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_swap(swap: &Htlc) {
|
||||||
|
storage::set_with_suffix(keys::SWAPS, &swap.id.to_le_bytes(), swap);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_swap_counter() -> u64 {
|
||||||
|
storage::get::<u64>(keys::SWAP_COUNTER).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_swap_id() -> u64 {
|
||||||
|
let id = get_swap_counter() + 1;
|
||||||
|
storage::set(keys::SWAP_COUNTER, &id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_locked_total() -> u128 {
|
||||||
|
storage::get::<u128>(keys::LOCKED_TOTAL).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_locked_total(amount: u128) {
|
||||||
|
storage::set(keys::LOCKED_TOTAL, &amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_escrow(user: &Address) -> u128 {
|
||||||
|
storage::get_with_suffix::<u128>(keys::ESCROW, user.as_bytes())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_escrow(user: &Address, amount: u128) {
|
||||||
|
storage::set_with_suffix(keys::ESCROW, user.as_bytes(), &amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute SHA256 hash (simplified - in production use crypto host function)
|
||||||
|
fn sha256(data: &[u8]) -> [u8; 32] {
|
||||||
|
// Simple hash for demo - in production, call crypto host function
|
||||||
|
let mut hash = [0u8; 32];
|
||||||
|
for (i, byte) in data.iter().enumerate() {
|
||||||
|
hash[i % 32] ^= byte;
|
||||||
|
hash[(i + 1) % 32] = hash[(i + 1) % 32].wrapping_add(*byte);
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ENTRY POINTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
synor_sdk::entry_point!(init, call);
|
||||||
|
|
||||||
|
/// Initialize the IBC bridge contract
|
||||||
|
fn init(params: &[u8]) -> Result<()> {
|
||||||
|
require!(!is_initialized(), Error::invalid_args("Already initialized"));
|
||||||
|
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct InitParams {
|
||||||
|
chain_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = InitParams::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (chain_id: String)"))?;
|
||||||
|
|
||||||
|
require!(
|
||||||
|
!params.chain_id.is_empty(),
|
||||||
|
Error::invalid_args("Chain ID cannot be empty")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
storage::set(keys::ADMIN, &caller());
|
||||||
|
storage::set(keys::INITIALIZED, &true);
|
||||||
|
storage::set(keys::CHAIN_ID, ¶ms.chain_id);
|
||||||
|
set_locked_total(0);
|
||||||
|
|
||||||
|
// Admin is automatically a relayer
|
||||||
|
storage::set_with_suffix(keys::RELAYERS, caller().as_bytes(), &true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle contract method calls
|
||||||
|
fn call(selector: &[u8], params: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
// Method selectors
|
||||||
|
let admin_sel = synor_sdk::method_selector("admin");
|
||||||
|
let add_relayer_sel = synor_sdk::method_selector("add_relayer");
|
||||||
|
let remove_relayer_sel = synor_sdk::method_selector("remove_relayer");
|
||||||
|
let register_channel_sel = synor_sdk::method_selector("register_channel");
|
||||||
|
let lock_tokens_sel = synor_sdk::method_selector("lock_tokens");
|
||||||
|
let unlock_tokens_sel = synor_sdk::method_selector("unlock_tokens");
|
||||||
|
let timeout_packet_sel = synor_sdk::method_selector("timeout_packet");
|
||||||
|
let create_swap_sel = synor_sdk::method_selector("create_swap");
|
||||||
|
let claim_swap_sel = synor_sdk::method_selector("claim_swap");
|
||||||
|
let refund_swap_sel = synor_sdk::method_selector("refund_swap");
|
||||||
|
let get_swap_sel = synor_sdk::method_selector("get_swap");
|
||||||
|
let get_channel_sel = synor_sdk::method_selector("get_channel");
|
||||||
|
let get_locked_sel = synor_sdk::method_selector("get_locked_total");
|
||||||
|
let get_escrow_sel = synor_sdk::method_selector("get_escrow");
|
||||||
|
|
||||||
|
match selector {
|
||||||
|
// ===== Admin Methods =====
|
||||||
|
|
||||||
|
s if s == admin_sel => {
|
||||||
|
let admin = get_admin().unwrap_or(Address::zero());
|
||||||
|
Ok(borsh::to_vec(&admin).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == add_relayer_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
relayer: Address,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (relayer: Address)"))?;
|
||||||
|
|
||||||
|
let admin = get_admin().ok_or(Error::Unauthorized)?;
|
||||||
|
require_auth!(admin);
|
||||||
|
|
||||||
|
require!(
|
||||||
|
args.relayer != Address::zero(),
|
||||||
|
Error::invalid_args("Cannot add zero address")
|
||||||
|
);
|
||||||
|
|
||||||
|
storage::set_with_suffix(keys::RELAYERS, args.relayer.as_bytes(), &true);
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::RELAYER_ADDED],
|
||||||
|
&borsh::to_vec(&RelayerAddedData {
|
||||||
|
relayer: args.relayer,
|
||||||
|
added_by: caller(),
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == remove_relayer_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
relayer: Address,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (relayer: Address)"))?;
|
||||||
|
|
||||||
|
let admin = get_admin().ok_or(Error::Unauthorized)?;
|
||||||
|
require_auth!(admin);
|
||||||
|
|
||||||
|
storage::set_with_suffix(keys::RELAYERS, args.relayer.as_bytes(), &false);
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::RELAYER_REMOVED],
|
||||||
|
&borsh::to_vec(&RelayerRemovedData {
|
||||||
|
relayer: args.relayer,
|
||||||
|
removed_by: caller(),
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == register_channel_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
channel_id: String,
|
||||||
|
counterparty_channel: String,
|
||||||
|
counterparty_chain: String,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (channel_id, counterparty_channel, counterparty_chain)"))?;
|
||||||
|
|
||||||
|
let admin = get_admin().ok_or(Error::Unauthorized)?;
|
||||||
|
require_auth!(admin);
|
||||||
|
|
||||||
|
require!(
|
||||||
|
!args.channel_id.is_empty(),
|
||||||
|
Error::invalid_args("Channel ID cannot be empty")
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = ChannelConfig::new(
|
||||||
|
args.channel_id.clone(),
|
||||||
|
args.counterparty_channel.clone(),
|
||||||
|
args.counterparty_chain.clone(),
|
||||||
|
);
|
||||||
|
set_channel(&config);
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::CHANNEL_REGISTERED],
|
||||||
|
&borsh::to_vec(&ChannelRegisteredData {
|
||||||
|
channel_id: args.channel_id,
|
||||||
|
counterparty_chain: args.counterparty_chain,
|
||||||
|
counterparty_channel: args.counterparty_channel,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ICS-20 Token Transfer Methods =====
|
||||||
|
|
||||||
|
s if s == lock_tokens_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
amount: u128,
|
||||||
|
receiver: String,
|
||||||
|
channel_id: String,
|
||||||
|
timeout_height: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (amount, receiver, channel_id, timeout_height)"))?;
|
||||||
|
|
||||||
|
require!(args.amount > 0, Error::invalid_args("Amount must be > 0"));
|
||||||
|
require!(
|
||||||
|
!args.receiver.is_empty(),
|
||||||
|
Error::invalid_args("Receiver cannot be empty")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get and validate channel
|
||||||
|
let mut channel = get_channel(&args.channel_id)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Channel not registered"))?;
|
||||||
|
require!(channel.active, Error::invalid_args("Channel not active"));
|
||||||
|
|
||||||
|
// Transfer tokens to this contract (escrow)
|
||||||
|
let sender = caller();
|
||||||
|
let current_escrow = get_escrow(&sender);
|
||||||
|
let new_escrow = current_escrow.checked_add(args.amount)
|
||||||
|
.ok_or(Error::Overflow)?;
|
||||||
|
set_escrow(&sender, new_escrow);
|
||||||
|
|
||||||
|
// Update locked total
|
||||||
|
let total = get_locked_total();
|
||||||
|
set_locked_total(total.checked_add(args.amount).ok_or(Error::Overflow)?);
|
||||||
|
|
||||||
|
// Create packet commitment
|
||||||
|
let sequence = channel.next_send_seq;
|
||||||
|
channel.next_send_seq += 1;
|
||||||
|
set_channel(&channel);
|
||||||
|
|
||||||
|
let packet_data = TokenPacket {
|
||||||
|
denom: String::from("SYNOR"),
|
||||||
|
amount: args.amount,
|
||||||
|
sender,
|
||||||
|
receiver: args.receiver.clone(),
|
||||||
|
memo: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_hash = sha256(&borsh::to_vec(&packet_data).unwrap());
|
||||||
|
|
||||||
|
let commitment = PacketCommitment {
|
||||||
|
sequence,
|
||||||
|
data_hash,
|
||||||
|
timeout_height: args.timeout_height,
|
||||||
|
timeout_timestamp: timestamp() + 3600_000_000_000, // 1 hour
|
||||||
|
source_channel: args.channel_id.clone(),
|
||||||
|
dest_channel: channel.counterparty_channel.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
storage::set_with_suffix(
|
||||||
|
keys::COMMITMENTS,
|
||||||
|
&commitment_key(&args.channel_id, sequence),
|
||||||
|
&commitment,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::TOKENS_LOCKED],
|
||||||
|
&borsh::to_vec(&TokensLockedData {
|
||||||
|
sender,
|
||||||
|
receiver: args.receiver,
|
||||||
|
amount: args.amount,
|
||||||
|
channel: args.channel_id,
|
||||||
|
sequence,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&sequence).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == unlock_tokens_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
packet_data: TokenPacket,
|
||||||
|
source_channel: String,
|
||||||
|
sequence: u64,
|
||||||
|
proof: Vec<u8>,
|
||||||
|
proof_height: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected packet data and proof"))?;
|
||||||
|
|
||||||
|
// Only relayers can submit proofs
|
||||||
|
require!(
|
||||||
|
is_relayer(&caller()),
|
||||||
|
Error::Unauthorized
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify channel exists
|
||||||
|
let mut channel = get_channel(&args.source_channel)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Channel not registered"))?;
|
||||||
|
require!(channel.active, Error::invalid_args("Channel not active"));
|
||||||
|
|
||||||
|
// Check sequence (prevent replay)
|
||||||
|
require!(
|
||||||
|
args.sequence == channel.next_recv_seq,
|
||||||
|
Error::invalid_args("Invalid sequence number")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify proof (simplified - real implementation verifies Merkle proof)
|
||||||
|
require!(
|
||||||
|
!args.proof.is_empty(),
|
||||||
|
Error::invalid_args("Proof required")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse receiver address (expects hex-encoded 34-byte address)
|
||||||
|
let receiver = Address::from_hex(&args.packet_data.receiver)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Invalid receiver address"))?;
|
||||||
|
|
||||||
|
// Update sequence
|
||||||
|
channel.next_recv_seq += 1;
|
||||||
|
set_channel(&channel);
|
||||||
|
|
||||||
|
// Record receipt
|
||||||
|
storage::set_with_suffix(
|
||||||
|
keys::RECEIPTS,
|
||||||
|
&commitment_key(&args.source_channel, args.sequence),
|
||||||
|
&true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transfer tokens to receiver
|
||||||
|
// In production, this would call token_transfer host function
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::TOKENS_UNLOCKED],
|
||||||
|
&borsh::to_vec(&TokensUnlockedData {
|
||||||
|
receiver,
|
||||||
|
amount: args.packet_data.amount,
|
||||||
|
source_channel: args.source_channel,
|
||||||
|
sequence: args.sequence,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == timeout_packet_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
channel_id: String,
|
||||||
|
sequence: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (channel_id, sequence)"))?;
|
||||||
|
|
||||||
|
// Get commitment
|
||||||
|
let commitment_k = commitment_key(&args.channel_id, args.sequence);
|
||||||
|
let commitment: PacketCommitment = storage::get_with_suffix(keys::COMMITMENTS, &commitment_k)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Packet commitment not found"))?;
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
let current_time = timestamp();
|
||||||
|
require!(
|
||||||
|
current_time >= commitment.timeout_timestamp,
|
||||||
|
Error::invalid_args("Packet not yet timed out")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get packet data to refund
|
||||||
|
// In production, we'd store more info or parse from commitment
|
||||||
|
let sender = caller(); // Simplified - should verify sender
|
||||||
|
|
||||||
|
// Refund tokens
|
||||||
|
let escrow = get_escrow(&sender);
|
||||||
|
// In production, look up actual locked amount from packet
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::PACKET_TIMEOUT],
|
||||||
|
&borsh::to_vec(&PacketTimeoutData {
|
||||||
|
sender,
|
||||||
|
amount: 0, // Would be actual amount
|
||||||
|
channel: args.channel_id,
|
||||||
|
sequence: args.sequence,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove commitment
|
||||||
|
storage::delete_with_suffix(keys::COMMITMENTS, &commitment_k);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Atomic Swap Methods =====
|
||||||
|
|
||||||
|
s if s == create_swap_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
hashlock: [u8; 32],
|
||||||
|
receiver: Address,
|
||||||
|
amount: u128,
|
||||||
|
timelock_seconds: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (hashlock, receiver, amount, timelock_seconds)"))?;
|
||||||
|
|
||||||
|
require!(args.amount > 0, Error::invalid_args("Amount must be > 0"));
|
||||||
|
require!(
|
||||||
|
args.timelock_seconds >= 3600,
|
||||||
|
Error::invalid_args("Timelock must be at least 1 hour")
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
args.receiver != Address::zero(),
|
||||||
|
Error::invalid_args("Invalid receiver")
|
||||||
|
);
|
||||||
|
|
||||||
|
let sender = caller();
|
||||||
|
let id = next_swap_id();
|
||||||
|
let timelock = timestamp() + args.timelock_seconds * 1_000_000_000;
|
||||||
|
|
||||||
|
let swap = Htlc {
|
||||||
|
id,
|
||||||
|
sender,
|
||||||
|
receiver: args.receiver,
|
||||||
|
amount: args.amount,
|
||||||
|
hashlock: args.hashlock,
|
||||||
|
timelock,
|
||||||
|
state: SwapState::Active,
|
||||||
|
secret: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
set_swap(&swap);
|
||||||
|
|
||||||
|
// Lock tokens
|
||||||
|
let escrow = get_escrow(&sender);
|
||||||
|
set_escrow(&sender, escrow.checked_add(args.amount).ok_or(Error::Overflow)?);
|
||||||
|
set_locked_total(get_locked_total().checked_add(args.amount).ok_or(Error::Overflow)?);
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::SWAP_CREATED],
|
||||||
|
&borsh::to_vec(&SwapCreatedData {
|
||||||
|
id,
|
||||||
|
sender,
|
||||||
|
receiver: args.receiver,
|
||||||
|
amount: args.amount,
|
||||||
|
hashlock: args.hashlock,
|
||||||
|
timelock,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&id).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == claim_swap_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
swap_id: u64,
|
||||||
|
secret: [u8; 32],
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (swap_id, secret)"))?;
|
||||||
|
|
||||||
|
let mut swap = get_swap(args.swap_id)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Swap not found"))?;
|
||||||
|
|
||||||
|
// Verify can claim
|
||||||
|
require!(
|
||||||
|
swap.can_claim(timestamp()),
|
||||||
|
Error::invalid_args("Cannot claim: expired or already processed")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify secret matches hashlock
|
||||||
|
let computed_hash = sha256(&args.secret);
|
||||||
|
require!(
|
||||||
|
computed_hash == swap.hashlock,
|
||||||
|
Error::invalid_args("Invalid secret")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update swap state
|
||||||
|
swap.state = SwapState::Completed;
|
||||||
|
swap.secret = Some(args.secret);
|
||||||
|
set_swap(&swap);
|
||||||
|
|
||||||
|
// Transfer tokens to receiver
|
||||||
|
let sender_escrow = get_escrow(&swap.sender);
|
||||||
|
set_escrow(&swap.sender, sender_escrow.saturating_sub(swap.amount));
|
||||||
|
set_locked_total(get_locked_total().saturating_sub(swap.amount));
|
||||||
|
|
||||||
|
// In production, transfer to receiver via host function
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::SWAP_CLAIMED],
|
||||||
|
&borsh::to_vec(&SwapClaimedData {
|
||||||
|
id: args.swap_id,
|
||||||
|
claimer: caller(),
|
||||||
|
secret: args.secret,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == refund_swap_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
swap_id: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (swap_id)"))?;
|
||||||
|
|
||||||
|
let mut swap = get_swap(args.swap_id)
|
||||||
|
.ok_or_else(|| Error::invalid_args("Swap not found"))?;
|
||||||
|
|
||||||
|
// Verify can refund
|
||||||
|
require!(
|
||||||
|
swap.can_refund(timestamp()),
|
||||||
|
Error::invalid_args("Cannot refund: not expired or already processed")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only sender can refund
|
||||||
|
require!(
|
||||||
|
caller() == swap.sender,
|
||||||
|
Error::Unauthorized
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update swap state
|
||||||
|
swap.state = SwapState::Refunded;
|
||||||
|
set_swap(&swap);
|
||||||
|
|
||||||
|
// Return tokens to sender
|
||||||
|
let escrow = get_escrow(&swap.sender);
|
||||||
|
set_escrow(&swap.sender, escrow.saturating_sub(swap.amount));
|
||||||
|
set_locked_total(get_locked_total().saturating_sub(swap.amount));
|
||||||
|
|
||||||
|
// In production, transfer back to sender via host function
|
||||||
|
|
||||||
|
emit_raw(
|
||||||
|
&[event_topics::SWAP_REFUNDED],
|
||||||
|
&borsh::to_vec(&SwapRefundedData {
|
||||||
|
id: args.swap_id,
|
||||||
|
sender: swap.sender,
|
||||||
|
amount: swap.amount,
|
||||||
|
}).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(borsh::to_vec(&true).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Read Methods =====
|
||||||
|
|
||||||
|
s if s == get_swap_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
swap_id: u64,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (swap_id)"))?;
|
||||||
|
|
||||||
|
let swap = get_swap(args.swap_id);
|
||||||
|
Ok(borsh::to_vec(&swap).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == get_channel_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
channel_id: String,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (channel_id)"))?;
|
||||||
|
|
||||||
|
let channel = get_channel(&args.channel_id);
|
||||||
|
Ok(borsh::to_vec(&channel).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == get_locked_sel => {
|
||||||
|
let total = get_locked_total();
|
||||||
|
Ok(borsh::to_vec(&total).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
s if s == get_escrow_sel => {
|
||||||
|
#[derive(BorshDeserialize)]
|
||||||
|
struct Args {
|
||||||
|
user: Address,
|
||||||
|
}
|
||||||
|
let args = Args::try_from_slice(params)
|
||||||
|
.map_err(|_| Error::invalid_args("Expected (user: Address)"))?;
|
||||||
|
|
||||||
|
let escrow = get_escrow(&args.user);
|
||||||
|
Ok(borsh::to_vec(&escrow).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Err(Error::InvalidMethod),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Generate commitment storage key
|
||||||
|
fn commitment_key(channel_id: &str, sequence: u64) -> Vec<u8> {
|
||||||
|
let mut key = channel_id.as_bytes().to_vec();
|
||||||
|
key.extend_from_slice(b":");
|
||||||
|
key.extend_from_slice(&sequence.to_le_bytes());
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode hex string to bytes
|
||||||
|
fn hex_decode(s: &str) -> core::result::Result<Vec<u8>, ()> {
|
||||||
|
if s.len() % 2 != 0 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = if s.starts_with("0x") || s.starts_with("0X") {
|
||||||
|
&s[2..]
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut bytes = Vec::with_capacity(s.len() / 2);
|
||||||
|
for chunk in s.as_bytes().chunks(2) {
|
||||||
|
let high = hex_char_to_nibble(chunk[0])?;
|
||||||
|
let low = hex_char_to_nibble(chunk[1])?;
|
||||||
|
bytes.push((high << 4) | low);
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_char_to_nibble(c: u8) -> core::result::Result<u8, ()> {
|
||||||
|
match c {
|
||||||
|
b'0'..=b'9' => Ok(c - b'0'),
|
||||||
|
b'a'..=b'f' => Ok(c - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Ok(c - b'A' + 10),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
46
crates/synor-ibc/Cargo.toml
Normal file
46
crates/synor-ibc/Cargo.toml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
[package]
|
||||||
|
name = "synor-ibc"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
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" }
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
borsh = { workspace = true }
|
||||||
|
|
||||||
|
# Cryptography
|
||||||
|
sha2 = "0.10"
|
||||||
|
blake3 = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Protobuf (for IBC compatibility)
|
||||||
|
prost = "0.13"
|
||||||
|
prost-types = "0.13"
|
||||||
|
|
||||||
|
# Time handling
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
tokio-test = "0.4"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.13"
|
||||||
768
crates/synor-ibc/src/channel.rs
Normal file
768
crates/synor-ibc/src/channel.rs
Normal file
|
|
@ -0,0 +1,768 @@
|
||||||
|
//! IBC Channel Management
|
||||||
|
//!
|
||||||
|
//! Channels provide the transport layer for IBC packets. Each channel
|
||||||
|
//! is bound to a specific port and connection.
|
||||||
|
//!
|
||||||
|
//! # Channel Types
|
||||||
|
//!
|
||||||
|
//! - **Ordered**: Packets must be received in order (sequence numbers)
|
||||||
|
//! - **Unordered**: Packets can be received in any order
|
||||||
|
//!
|
||||||
|
//! # Channel Handshake (4-way)
|
||||||
|
//!
|
||||||
|
//! Similar to connection handshake:
|
||||||
|
//! 1. ChanOpenInit
|
||||||
|
//! 2. ChanOpenTry
|
||||||
|
//! 3. ChanOpenAck
|
||||||
|
//! 4. ChanOpenConfirm
|
||||||
|
|
||||||
|
use crate::connection::ConnectionId;
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::types::Height;
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Port identifier
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct PortId(pub String);
|
||||||
|
|
||||||
|
impl PortId {
|
||||||
|
/// Create a new port ID
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
Self(id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfer port
|
||||||
|
pub fn transfer() -> Self {
|
||||||
|
Self("transfer".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interchain accounts host port
|
||||||
|
pub fn ica_host() -> Self {
|
||||||
|
Self("icahost".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interchain accounts controller port
|
||||||
|
pub fn ica_controller(owner: &str) -> Self {
|
||||||
|
Self(format!("icacontroller-{}", owner))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate port ID
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.0.is_empty() {
|
||||||
|
return Err(IbcError::InvalidIdentifier("port ID cannot be empty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.0.len() > 128 {
|
||||||
|
return Err(IbcError::InvalidIdentifier("port ID too long".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alphanumeric and limited special characters
|
||||||
|
if !self.0.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
|
||||||
|
return Err(IbcError::InvalidIdentifier(
|
||||||
|
"port ID contains invalid characters".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PortId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel identifier
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct ChannelId(pub String);
|
||||||
|
|
||||||
|
impl ChannelId {
|
||||||
|
/// Create a new channel ID
|
||||||
|
pub fn new(sequence: u64) -> Self {
|
||||||
|
Self(format!("channel-{}", sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse sequence from ID
|
||||||
|
pub fn sequence(&self) -> Option<u64> {
|
||||||
|
self.0.strip_prefix("channel-")?.parse().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ChannelId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel ordering
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ChannelOrder {
|
||||||
|
/// Packets can arrive in any order
|
||||||
|
Unordered,
|
||||||
|
/// Packets must arrive in sequence order
|
||||||
|
Ordered,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelOrder {
|
||||||
|
/// Get order name
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChannelOrder::Unordered => "ORDER_UNORDERED",
|
||||||
|
ChannelOrder::Ordered => "ORDER_ORDERED",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ChannelOrder {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel state
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ChannelState {
|
||||||
|
/// Uninitialized (default)
|
||||||
|
Uninitialized,
|
||||||
|
/// Init: ChanOpenInit sent
|
||||||
|
Init,
|
||||||
|
/// TryOpen: ChanOpenTry sent
|
||||||
|
TryOpen,
|
||||||
|
/// Open: Channel fully established
|
||||||
|
Open,
|
||||||
|
/// Closed: Channel closed
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelState {
|
||||||
|
/// Get state name
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChannelState::Uninitialized => "UNINITIALIZED",
|
||||||
|
ChannelState::Init => "INIT",
|
||||||
|
ChannelState::TryOpen => "TRYOPEN",
|
||||||
|
ChannelState::Open => "OPEN",
|
||||||
|
ChannelState::Closed => "CLOSED",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ChannelState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel counterparty
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ChannelCounterparty {
|
||||||
|
/// Counterparty port ID
|
||||||
|
pub port_id: PortId,
|
||||||
|
/// Counterparty channel ID
|
||||||
|
pub channel_id: Option<ChannelId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelCounterparty {
|
||||||
|
/// Create a new counterparty
|
||||||
|
pub fn new(port_id: PortId, channel_id: Option<ChannelId>) -> Self {
|
||||||
|
Self { port_id, channel_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel end state
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Channel {
|
||||||
|
/// Channel state
|
||||||
|
pub state: ChannelState,
|
||||||
|
/// Channel ordering
|
||||||
|
pub ordering: ChannelOrder,
|
||||||
|
/// Counterparty information
|
||||||
|
pub counterparty: ChannelCounterparty,
|
||||||
|
/// Connection hops (usually single connection)
|
||||||
|
pub connection_hops: Vec<ConnectionId>,
|
||||||
|
/// Channel version (app-specific)
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
/// Create a new channel in Init state
|
||||||
|
pub fn new_init(
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
counterparty: ChannelCounterparty,
|
||||||
|
connection_hops: Vec<ConnectionId>,
|
||||||
|
version: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
state: ChannelState::Init,
|
||||||
|
ordering,
|
||||||
|
counterparty,
|
||||||
|
connection_hops,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new channel in TryOpen state
|
||||||
|
pub fn new_try_open(
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
counterparty: ChannelCounterparty,
|
||||||
|
connection_hops: Vec<ConnectionId>,
|
||||||
|
version: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
state: ChannelState::TryOpen,
|
||||||
|
ordering,
|
||||||
|
counterparty,
|
||||||
|
connection_hops,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if channel is open
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.state == ChannelState::Open
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if channel is closed
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.state == ChannelState::Closed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set state to TryOpen
|
||||||
|
pub fn set_try_open(&mut self) {
|
||||||
|
self.state = ChannelState::TryOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set state to Open
|
||||||
|
pub fn set_open(&mut self) {
|
||||||
|
self.state = ChannelState::Open;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set state to Closed
|
||||||
|
pub fn set_closed(&mut self) {
|
||||||
|
self.state = ChannelState::Closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set counterparty channel ID
|
||||||
|
pub fn set_counterparty_channel_id(&mut self, id: ChannelId) {
|
||||||
|
self.counterparty.channel_id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the channel
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.connection_hops.is_empty() {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: "at least one connection hop".to_string(),
|
||||||
|
actual: "none".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.counterparty.port_id.validate()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel key (port, channel)
|
||||||
|
pub type ChannelKey = (PortId, ChannelId);
|
||||||
|
|
||||||
|
/// Sequence counters for a channel
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ChannelSequences {
|
||||||
|
/// Next sequence to send
|
||||||
|
pub next_send: u64,
|
||||||
|
/// Next sequence to receive
|
||||||
|
pub next_recv: u64,
|
||||||
|
/// Next sequence to acknowledge
|
||||||
|
pub next_ack: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel manager
|
||||||
|
pub struct ChannelManager {
|
||||||
|
/// Channels by (port, channel)
|
||||||
|
channels: HashMap<ChannelKey, Channel>,
|
||||||
|
/// Sequence counters by (port, channel)
|
||||||
|
sequences: HashMap<ChannelKey, ChannelSequences>,
|
||||||
|
/// Port bindings (port -> module name)
|
||||||
|
port_bindings: HashMap<PortId, String>,
|
||||||
|
/// Next channel sequence per port
|
||||||
|
next_channel_sequence: HashMap<PortId, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelManager {
|
||||||
|
/// Create a new channel manager
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
channels: HashMap::new(),
|
||||||
|
sequences: HashMap::new(),
|
||||||
|
port_bindings: HashMap::new(),
|
||||||
|
next_channel_sequence: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind a port to a module
|
||||||
|
pub fn bind_port(&mut self, port_id: PortId, module: String) -> IbcResult<()> {
|
||||||
|
port_id.validate()?;
|
||||||
|
|
||||||
|
if self.port_bindings.contains_key(&port_id) {
|
||||||
|
return Err(IbcError::PortAlreadyBound(port_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.port_bindings.insert(port_id, module);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release a port binding
|
||||||
|
pub fn release_port(&mut self, port_id: &PortId) -> IbcResult<()> {
|
||||||
|
if self.port_bindings.remove(port_id).is_none() {
|
||||||
|
return Err(IbcError::PortNotBound(port_id.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if port is bound
|
||||||
|
pub fn is_port_bound(&self, port_id: &PortId) -> bool {
|
||||||
|
self.port_bindings.contains_key(port_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate next channel ID for a port
|
||||||
|
fn next_channel_id(&mut self, port_id: &PortId) -> ChannelId {
|
||||||
|
let seq = self.next_channel_sequence.entry(port_id.clone()).or_insert(0);
|
||||||
|
let id = ChannelId::new(*seq);
|
||||||
|
*seq += 1;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a channel
|
||||||
|
pub fn get_channel(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<&Channel> {
|
||||||
|
self.channels
|
||||||
|
.get(&(port_id.clone(), channel_id.clone()))
|
||||||
|
.ok_or_else(|| IbcError::ChannelNotFound {
|
||||||
|
port: port_id.to_string(),
|
||||||
|
channel: channel_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable channel
|
||||||
|
fn get_channel_mut(
|
||||||
|
&mut self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
) -> IbcResult<&mut Channel> {
|
||||||
|
self.channels
|
||||||
|
.get_mut(&(port_id.clone(), channel_id.clone()))
|
||||||
|
.ok_or_else(|| IbcError::ChannelNotFound {
|
||||||
|
port: port_id.to_string(),
|
||||||
|
channel: channel_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get sequences for a channel
|
||||||
|
pub fn get_sequences(&self, port_id: &PortId, channel_id: &ChannelId) -> ChannelSequences {
|
||||||
|
self.sequences
|
||||||
|
.get(&(port_id.clone(), channel_id.clone()))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment send sequence and return previous value
|
||||||
|
pub fn increment_send_sequence(
|
||||||
|
&mut self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
) -> u64 {
|
||||||
|
let key = (port_id.clone(), channel_id.clone());
|
||||||
|
let seq = self.sequences.entry(key).or_default();
|
||||||
|
let current = seq.next_send;
|
||||||
|
seq.next_send += 1;
|
||||||
|
current
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment receive sequence
|
||||||
|
pub fn increment_recv_sequence(&mut self, port_id: &PortId, channel_id: &ChannelId) {
|
||||||
|
let key = (port_id.clone(), channel_id.clone());
|
||||||
|
let seq = self.sequences.entry(key).or_default();
|
||||||
|
seq.next_recv += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment ack sequence
|
||||||
|
pub fn increment_ack_sequence(&mut self, port_id: &PortId, channel_id: &ChannelId) {
|
||||||
|
let key = (port_id.clone(), channel_id.clone());
|
||||||
|
let seq = self.sequences.entry(key).or_default();
|
||||||
|
seq.next_ack += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Open Init
|
||||||
|
pub fn chan_open_init(
|
||||||
|
&mut self,
|
||||||
|
port_id: PortId,
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
connection_hops: Vec<ConnectionId>,
|
||||||
|
counterparty_port: PortId,
|
||||||
|
version: String,
|
||||||
|
) -> IbcResult<ChannelId> {
|
||||||
|
// Validate port
|
||||||
|
port_id.validate()?;
|
||||||
|
counterparty_port.validate()?;
|
||||||
|
|
||||||
|
// Check port is bound
|
||||||
|
if !self.is_port_bound(&port_id) {
|
||||||
|
return Err(IbcError::PortNotBound(port_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate channel ID
|
||||||
|
let channel_id = self.next_channel_id(&port_id);
|
||||||
|
|
||||||
|
// Create channel
|
||||||
|
let counterparty = ChannelCounterparty::new(counterparty_port, None);
|
||||||
|
let channel = Channel::new_init(ordering, counterparty, connection_hops, version);
|
||||||
|
channel.validate()?;
|
||||||
|
|
||||||
|
// Store channel
|
||||||
|
let key = (port_id.clone(), channel_id.clone());
|
||||||
|
self.channels.insert(key.clone(), channel);
|
||||||
|
self.sequences.insert(key, ChannelSequences {
|
||||||
|
next_send: 1,
|
||||||
|
next_recv: 1,
|
||||||
|
next_ack: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel init complete"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Open Try
|
||||||
|
pub fn chan_open_try(
|
||||||
|
&mut self,
|
||||||
|
port_id: PortId,
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
connection_hops: Vec<ConnectionId>,
|
||||||
|
counterparty_port: PortId,
|
||||||
|
counterparty_channel: ChannelId,
|
||||||
|
version: String,
|
||||||
|
_counterparty_version: String,
|
||||||
|
proof_init: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<ChannelId> {
|
||||||
|
// Validate
|
||||||
|
port_id.validate()?;
|
||||||
|
|
||||||
|
// Check port is bound
|
||||||
|
if !self.is_port_bound(&port_id) {
|
||||||
|
return Err(IbcError::PortNotBound(port_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_init.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_init required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate channel ID
|
||||||
|
let channel_id = self.next_channel_id(&port_id);
|
||||||
|
|
||||||
|
// Create channel
|
||||||
|
let counterparty = ChannelCounterparty::new(counterparty_port, Some(counterparty_channel));
|
||||||
|
let channel = Channel::new_try_open(ordering, counterparty, connection_hops, version);
|
||||||
|
channel.validate()?;
|
||||||
|
|
||||||
|
// Store channel
|
||||||
|
let key = (port_id.clone(), channel_id.clone());
|
||||||
|
self.channels.insert(key.clone(), channel);
|
||||||
|
self.sequences.insert(key, ChannelSequences {
|
||||||
|
next_send: 1,
|
||||||
|
next_recv: 1,
|
||||||
|
next_ack: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel try open complete"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Open Ack
|
||||||
|
pub fn chan_open_ack(
|
||||||
|
&mut self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
counterparty_channel: ChannelId,
|
||||||
|
counterparty_version: String,
|
||||||
|
proof_try: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let channel = self.get_channel_mut(port_id, channel_id)?;
|
||||||
|
|
||||||
|
// Verify state
|
||||||
|
if channel.state != ChannelState::Init {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: ChannelState::Init.to_string(),
|
||||||
|
actual: channel.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_try.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_try required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel
|
||||||
|
channel.set_counterparty_channel_id(counterparty_channel);
|
||||||
|
channel.version = counterparty_version;
|
||||||
|
channel.set_open();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel ack complete - channel OPEN"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Open Confirm
|
||||||
|
pub fn chan_open_confirm(
|
||||||
|
&mut self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
proof_ack: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let channel = self.get_channel_mut(port_id, channel_id)?;
|
||||||
|
|
||||||
|
// Verify state
|
||||||
|
if channel.state != ChannelState::TryOpen {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: ChannelState::TryOpen.to_string(),
|
||||||
|
actual: channel.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_ack.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_ack required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
channel.set_open();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel confirm complete - channel OPEN"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Close Init
|
||||||
|
pub fn chan_close_init(&mut self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<()> {
|
||||||
|
let channel = self.get_channel_mut(port_id, channel_id)?;
|
||||||
|
|
||||||
|
if !channel.is_open() {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: ChannelState::Open.to_string(),
|
||||||
|
actual: channel.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.set_closed();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel close init - channel CLOSED"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel Close Confirm
|
||||||
|
pub fn chan_close_confirm(
|
||||||
|
&mut self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
proof_init: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let channel = self.get_channel_mut(port_id, channel_id)?;
|
||||||
|
|
||||||
|
if channel.is_closed() {
|
||||||
|
return Ok(()); // Already closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_init.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_init required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.set_closed();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %port_id,
|
||||||
|
channel = %channel_id,
|
||||||
|
"Channel close confirm - channel CLOSED"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all channels
|
||||||
|
pub fn all_channels(&self) -> impl Iterator<Item = (&ChannelKey, &Channel)> {
|
||||||
|
self.channels.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get channels for a port
|
||||||
|
pub fn port_channels(&self, port_id: &PortId) -> Vec<(ChannelId, Channel)> {
|
||||||
|
self.channels
|
||||||
|
.iter()
|
||||||
|
.filter(|((p, _), _)| p == port_id)
|
||||||
|
.map(|((_, c), ch)| (c.clone(), ch.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChannelManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_port_id() {
|
||||||
|
let port = PortId::transfer();
|
||||||
|
assert_eq!(port.to_string(), "transfer");
|
||||||
|
assert!(port.validate().is_ok());
|
||||||
|
|
||||||
|
let invalid = PortId::new("");
|
||||||
|
assert!(invalid.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_channel_id() {
|
||||||
|
let id = ChannelId::new(5);
|
||||||
|
assert_eq!(id.to_string(), "channel-5");
|
||||||
|
assert_eq!(id.sequence(), Some(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_port_binding() {
|
||||||
|
let mut manager = ChannelManager::new();
|
||||||
|
let port = PortId::transfer();
|
||||||
|
|
||||||
|
assert!(manager.bind_port(port.clone(), "transfer".to_string()).is_ok());
|
||||||
|
assert!(manager.is_port_bound(&port));
|
||||||
|
|
||||||
|
// Can't bind same port twice
|
||||||
|
assert!(manager.bind_port(port.clone(), "other".to_string()).is_err());
|
||||||
|
|
||||||
|
// Release and rebind
|
||||||
|
assert!(manager.release_port(&port).is_ok());
|
||||||
|
assert!(!manager.is_port_bound(&port));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_channel_handshake() {
|
||||||
|
let mut manager_a = ChannelManager::new();
|
||||||
|
let mut manager_b = ChannelManager::new();
|
||||||
|
|
||||||
|
let port = PortId::transfer();
|
||||||
|
let conn = ConnectionId::new(0);
|
||||||
|
|
||||||
|
// Bind ports
|
||||||
|
manager_a.bind_port(port.clone(), "transfer".to_string()).unwrap();
|
||||||
|
manager_b.bind_port(port.clone(), "transfer".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Chain A: Init
|
||||||
|
let chan_a = manager_a
|
||||||
|
.chan_open_init(
|
||||||
|
port.clone(),
|
||||||
|
ChannelOrder::Unordered,
|
||||||
|
vec![conn.clone()],
|
||||||
|
port.clone(),
|
||||||
|
"ics20-1".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Chain B: TryOpen
|
||||||
|
let chan_b = manager_b
|
||||||
|
.chan_open_try(
|
||||||
|
port.clone(),
|
||||||
|
ChannelOrder::Unordered,
|
||||||
|
vec![conn.clone()],
|
||||||
|
port.clone(),
|
||||||
|
chan_a.clone(),
|
||||||
|
"ics20-1".to_string(),
|
||||||
|
"ics20-1".to_string(),
|
||||||
|
vec![1, 2, 3],
|
||||||
|
Height::from_height(100),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Chain A: Ack
|
||||||
|
manager_a
|
||||||
|
.chan_open_ack(
|
||||||
|
&port,
|
||||||
|
&chan_a,
|
||||||
|
chan_b.clone(),
|
||||||
|
"ics20-1".to_string(),
|
||||||
|
vec![1, 2, 3],
|
||||||
|
Height::from_height(101),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(manager_a.get_channel(&port, &chan_a).unwrap().is_open());
|
||||||
|
|
||||||
|
// Chain B: Confirm
|
||||||
|
manager_b
|
||||||
|
.chan_open_confirm(&port, &chan_b, vec![1, 2, 3], Height::from_height(102))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(manager_b.get_channel(&port, &chan_b).unwrap().is_open());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sequence_management() {
|
||||||
|
let mut manager = ChannelManager::new();
|
||||||
|
let port = PortId::transfer();
|
||||||
|
let conn = ConnectionId::new(0);
|
||||||
|
|
||||||
|
manager.bind_port(port.clone(), "transfer".to_string()).unwrap();
|
||||||
|
|
||||||
|
let channel = manager
|
||||||
|
.chan_open_init(
|
||||||
|
port.clone(),
|
||||||
|
ChannelOrder::Ordered,
|
||||||
|
vec![conn],
|
||||||
|
port.clone(),
|
||||||
|
"ics20-1".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Initial sequences should be 1
|
||||||
|
let seq = manager.get_sequences(&port, &channel);
|
||||||
|
assert_eq!(seq.next_send, 1);
|
||||||
|
assert_eq!(seq.next_recv, 1);
|
||||||
|
|
||||||
|
// Increment send
|
||||||
|
let send_seq = manager.increment_send_sequence(&port, &channel);
|
||||||
|
assert_eq!(send_seq, 1);
|
||||||
|
|
||||||
|
let seq = manager.get_sequences(&port, &channel);
|
||||||
|
assert_eq!(seq.next_send, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
600
crates/synor-ibc/src/client.rs
Normal file
600
crates/synor-ibc/src/client.rs
Normal file
|
|
@ -0,0 +1,600 @@
|
||||||
|
//! IBC Light Client
|
||||||
|
//!
|
||||||
|
//! Light clients enable trustless verification of state from remote chains.
|
||||||
|
//! Each chain type has its own light client implementation.
|
||||||
|
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::types::{ChainId, Height, Timestamp};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Client identifier
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct ClientId(pub String);
|
||||||
|
|
||||||
|
impl ClientId {
|
||||||
|
/// Create a new client ID
|
||||||
|
pub fn new(client_type: &str, sequence: u64) -> Self {
|
||||||
|
Self(format!("{}-{}", client_type, sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse client type from ID
|
||||||
|
pub fn client_type(&self) -> Option<&str> {
|
||||||
|
self.0.rsplit('-').nth(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse sequence from ID
|
||||||
|
pub fn sequence(&self) -> Option<u64> {
|
||||||
|
self.0.rsplit('-').next()?.parse().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ClientId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Light client type
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ClientType {
|
||||||
|
/// Tendermint/CometBFT light client (Cosmos chains)
|
||||||
|
Tendermint,
|
||||||
|
/// GRANDPA light client (Polkadot/Substrate)
|
||||||
|
Grandpa,
|
||||||
|
/// Synor native light client
|
||||||
|
Synor,
|
||||||
|
/// Solo machine (single signer)
|
||||||
|
SoloMachine,
|
||||||
|
/// Localhost (same chain, for testing)
|
||||||
|
Localhost,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientType {
|
||||||
|
/// Get type name
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ClientType::Tendermint => "07-tendermint",
|
||||||
|
ClientType::Grandpa => "10-grandpa",
|
||||||
|
ClientType::Synor => "synor",
|
||||||
|
ClientType::SoloMachine => "06-solomachine",
|
||||||
|
ClientType::Localhost => "09-localhost",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ClientType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client state - represents the state of a light client
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClientState {
|
||||||
|
/// Client type
|
||||||
|
pub client_type: ClientType,
|
||||||
|
/// Chain ID of the counterparty
|
||||||
|
pub chain_id: ChainId,
|
||||||
|
/// Trust level (numerator/denominator)
|
||||||
|
pub trust_level: TrustLevel,
|
||||||
|
/// Trusting period in nanoseconds
|
||||||
|
pub trusting_period: u64,
|
||||||
|
/// Unbonding period in nanoseconds
|
||||||
|
pub unbonding_period: u64,
|
||||||
|
/// Max clock drift in nanoseconds
|
||||||
|
pub max_clock_drift: u64,
|
||||||
|
/// Latest height
|
||||||
|
pub latest_height: Height,
|
||||||
|
/// Frozen height (None if not frozen)
|
||||||
|
pub frozen_height: Option<Height>,
|
||||||
|
/// Whether upgrades are allowed
|
||||||
|
pub allow_update_after_expiry: bool,
|
||||||
|
/// Whether misbehaviour upgrades are allowed
|
||||||
|
pub allow_update_after_misbehaviour: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientState {
|
||||||
|
/// Create a new Tendermint client state
|
||||||
|
pub fn new_tendermint(
|
||||||
|
chain_id: ChainId,
|
||||||
|
trust_level: TrustLevel,
|
||||||
|
trusting_period: u64,
|
||||||
|
unbonding_period: u64,
|
||||||
|
max_clock_drift: u64,
|
||||||
|
latest_height: Height,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
client_type: ClientType::Tendermint,
|
||||||
|
chain_id,
|
||||||
|
trust_level,
|
||||||
|
trusting_period,
|
||||||
|
unbonding_period,
|
||||||
|
max_clock_drift,
|
||||||
|
latest_height,
|
||||||
|
frozen_height: None,
|
||||||
|
allow_update_after_expiry: false,
|
||||||
|
allow_update_after_misbehaviour: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Synor client state
|
||||||
|
pub fn new_synor(chain_id: ChainId, latest_height: Height) -> Self {
|
||||||
|
Self {
|
||||||
|
client_type: ClientType::Synor,
|
||||||
|
chain_id,
|
||||||
|
trust_level: TrustLevel::default(),
|
||||||
|
trusting_period: 14 * 24 * 60 * 60 * 1_000_000_000, // 14 days
|
||||||
|
unbonding_period: 21 * 24 * 60 * 60 * 1_000_000_000, // 21 days
|
||||||
|
max_clock_drift: 10 * 1_000_000_000, // 10 seconds
|
||||||
|
latest_height,
|
||||||
|
frozen_height: None,
|
||||||
|
allow_update_after_expiry: true,
|
||||||
|
allow_update_after_misbehaviour: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if client is frozen
|
||||||
|
pub fn is_frozen(&self) -> bool {
|
||||||
|
self.frozen_height.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if client is expired
|
||||||
|
pub fn is_expired(&self, current_time: Timestamp, consensus_time: Timestamp) -> bool {
|
||||||
|
let elapsed = current_time.nanoseconds().saturating_sub(consensus_time.nanoseconds());
|
||||||
|
elapsed > self.trusting_period
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Freeze the client at a height
|
||||||
|
pub fn freeze(&mut self, height: Height) {
|
||||||
|
self.frozen_height = Some(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the client state is valid
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.trusting_period >= self.unbonding_period {
|
||||||
|
return Err(IbcError::InvalidClientState(
|
||||||
|
"trusting period must be less than unbonding period".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.latest_height.is_zero() {
|
||||||
|
return Err(IbcError::InvalidClientState(
|
||||||
|
"latest height cannot be zero".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.trust_level.validate()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trust level (fraction)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct TrustLevel {
|
||||||
|
/// Numerator
|
||||||
|
pub numerator: u64,
|
||||||
|
/// Denominator
|
||||||
|
pub denominator: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustLevel {
|
||||||
|
/// Create a new trust level
|
||||||
|
pub fn new(numerator: u64, denominator: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
numerator,
|
||||||
|
denominator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default trust level (1/3)
|
||||||
|
pub fn one_third() -> Self {
|
||||||
|
Self {
|
||||||
|
numerator: 1,
|
||||||
|
denominator: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two-thirds trust level
|
||||||
|
pub fn two_thirds() -> Self {
|
||||||
|
Self {
|
||||||
|
numerator: 2,
|
||||||
|
denominator: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the trust level
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.denominator == 0 {
|
||||||
|
return Err(IbcError::InvalidClientState(
|
||||||
|
"trust level denominator cannot be zero".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.numerator == 0 || self.numerator > self.denominator {
|
||||||
|
return Err(IbcError::InvalidClientState(
|
||||||
|
"trust level must be between 0 and 1".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a voting power meets the trust threshold
|
||||||
|
pub fn check(&self, signed_power: u64, total_power: u64) -> bool {
|
||||||
|
if total_power == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// signed_power / total_power >= numerator / denominator
|
||||||
|
// signed_power * denominator >= numerator * total_power
|
||||||
|
signed_power * self.denominator >= self.numerator * total_power
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TrustLevel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::one_third()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consensus state - represents the consensus state at a specific height
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConsensusState {
|
||||||
|
/// Timestamp of this state
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
/// Root hash of the state (commitment root)
|
||||||
|
pub root: Vec<u8>,
|
||||||
|
/// Next validators hash (for Tendermint)
|
||||||
|
pub next_validators_hash: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsensusState {
|
||||||
|
/// Create a new consensus state
|
||||||
|
pub fn new(timestamp: Timestamp, root: Vec<u8>, next_validators_hash: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
timestamp,
|
||||||
|
root,
|
||||||
|
next_validators_hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the root as a fixed-size array
|
||||||
|
pub fn root_bytes(&self) -> [u8; 32] {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
let len = self.root.len().min(32);
|
||||||
|
bytes[..len].copy_from_slice(&self.root[..len]);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Header for updating client state
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Header {
|
||||||
|
/// Height of this header
|
||||||
|
pub height: Height,
|
||||||
|
/// Timestamp
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
/// Signed header data
|
||||||
|
pub signed_header: SignedHeader,
|
||||||
|
/// Validator set
|
||||||
|
pub validator_set: ValidatorSet,
|
||||||
|
/// Trusted height (for verification)
|
||||||
|
pub trusted_height: Height,
|
||||||
|
/// Trusted validators
|
||||||
|
pub trusted_validators: ValidatorSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signed header
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SignedHeader {
|
||||||
|
/// Block hash
|
||||||
|
pub block_hash: Vec<u8>,
|
||||||
|
/// App hash (state root)
|
||||||
|
pub app_hash: Vec<u8>,
|
||||||
|
/// Commit signatures
|
||||||
|
pub commit: Commit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit with signatures
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Commit {
|
||||||
|
/// Height
|
||||||
|
pub height: Height,
|
||||||
|
/// Block ID
|
||||||
|
pub block_id: Vec<u8>,
|
||||||
|
/// Signatures
|
||||||
|
pub signatures: Vec<CommitSig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit signature
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CommitSig {
|
||||||
|
/// Validator address
|
||||||
|
pub validator_address: Vec<u8>,
|
||||||
|
/// Timestamp
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
/// Signature bytes
|
||||||
|
pub signature: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validator set
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ValidatorSet {
|
||||||
|
/// Validators
|
||||||
|
pub validators: Vec<Validator>,
|
||||||
|
/// Proposer address
|
||||||
|
pub proposer: Option<Vec<u8>>,
|
||||||
|
/// Total voting power
|
||||||
|
pub total_voting_power: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidatorSet {
|
||||||
|
/// Create an empty validator set
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
validators: Vec::new(),
|
||||||
|
proposer: None,
|
||||||
|
total_voting_power: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate total voting power
|
||||||
|
pub fn calculate_total_power(&mut self) {
|
||||||
|
self.total_voting_power = self.validators.iter().map(|v| v.voting_power).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get validator by address
|
||||||
|
pub fn get_by_address(&self, address: &[u8]) -> Option<&Validator> {
|
||||||
|
self.validators.iter().find(|v| v.address == address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validator
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Validator {
|
||||||
|
/// Address
|
||||||
|
pub address: Vec<u8>,
|
||||||
|
/// Public key
|
||||||
|
pub pub_key: Vec<u8>,
|
||||||
|
/// Voting power
|
||||||
|
pub voting_power: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Light client for verifying remote chain state
|
||||||
|
pub struct LightClient {
|
||||||
|
/// Client states by ID
|
||||||
|
client_states: HashMap<ClientId, ClientState>,
|
||||||
|
/// Consensus states by (client_id, height)
|
||||||
|
consensus_states: HashMap<(ClientId, Height), ConsensusState>,
|
||||||
|
/// Next client sequence
|
||||||
|
next_client_sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LightClient {
|
||||||
|
/// Create a new light client manager
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client_states: HashMap::new(),
|
||||||
|
consensus_states: HashMap::new(),
|
||||||
|
next_client_sequence: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new client
|
||||||
|
pub fn create_client(
|
||||||
|
&mut self,
|
||||||
|
client_state: ClientState,
|
||||||
|
consensus_state: ConsensusState,
|
||||||
|
) -> IbcResult<ClientId> {
|
||||||
|
// Validate client state
|
||||||
|
client_state.validate()?;
|
||||||
|
|
||||||
|
// Generate client ID
|
||||||
|
let client_id = ClientId::new(client_state.client_type.as_str(), self.next_client_sequence);
|
||||||
|
self.next_client_sequence += 1;
|
||||||
|
|
||||||
|
// Store states
|
||||||
|
let height = client_state.latest_height;
|
||||||
|
self.consensus_states
|
||||||
|
.insert((client_id.clone(), height), consensus_state);
|
||||||
|
self.client_states.insert(client_id.clone(), client_state);
|
||||||
|
|
||||||
|
Ok(client_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get client state
|
||||||
|
pub fn get_client_state(&self, client_id: &ClientId) -> IbcResult<&ClientState> {
|
||||||
|
self.client_states
|
||||||
|
.get(client_id)
|
||||||
|
.ok_or_else(|| IbcError::ClientNotFound(client_id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get consensus state at height
|
||||||
|
pub fn get_consensus_state(
|
||||||
|
&self,
|
||||||
|
client_id: &ClientId,
|
||||||
|
height: Height,
|
||||||
|
) -> IbcResult<&ConsensusState> {
|
||||||
|
self.consensus_states
|
||||||
|
.get(&(client_id.clone(), height))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
IbcError::InvalidClientState(format!(
|
||||||
|
"consensus state not found at height {}",
|
||||||
|
height
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update client with new header
|
||||||
|
pub fn update_client(&mut self, client_id: &ClientId, header: Header) -> IbcResult<()> {
|
||||||
|
// First check if client exists and is not frozen (immutable borrow)
|
||||||
|
{
|
||||||
|
let client_state = self
|
||||||
|
.client_states
|
||||||
|
.get(client_id)
|
||||||
|
.ok_or_else(|| IbcError::ClientNotFound(client_id.to_string()))?;
|
||||||
|
|
||||||
|
if client_state.is_frozen() {
|
||||||
|
return Err(IbcError::ClientFrozen(client_id.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify header (needs immutable borrow of self)
|
||||||
|
self.verify_header(client_id, &header)?;
|
||||||
|
|
||||||
|
// Now get mutable borrow to update client state
|
||||||
|
let client_state = self
|
||||||
|
.client_states
|
||||||
|
.get_mut(client_id)
|
||||||
|
.ok_or_else(|| IbcError::ClientNotFound(client_id.to_string()))?;
|
||||||
|
|
||||||
|
// Update client state
|
||||||
|
if header.height > client_state.latest_height {
|
||||||
|
client_state.latest_height = header.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store new consensus state
|
||||||
|
let consensus_state = ConsensusState {
|
||||||
|
timestamp: header.timestamp,
|
||||||
|
root: header.signed_header.app_hash.clone(),
|
||||||
|
next_validators_hash: Vec::new(), // Would be extracted from header
|
||||||
|
};
|
||||||
|
self.consensus_states
|
||||||
|
.insert((client_id.clone(), header.height), consensus_state);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a header against trusted state
|
||||||
|
fn verify_header(&self, client_id: &ClientId, header: &Header) -> IbcResult<()> {
|
||||||
|
let client_state = self.get_client_state(client_id)?;
|
||||||
|
let _trusted_consensus = self.get_consensus_state(client_id, header.trusted_height)?;
|
||||||
|
|
||||||
|
// Check header height is greater than trusted height
|
||||||
|
if header.height <= header.trusted_height {
|
||||||
|
return Err(IbcError::InvalidClientState(
|
||||||
|
"header height must be greater than trusted height".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timestamp is within max clock drift
|
||||||
|
let current_time = Timestamp::now();
|
||||||
|
let drift = header
|
||||||
|
.timestamp
|
||||||
|
.nanoseconds()
|
||||||
|
.saturating_sub(current_time.nanoseconds());
|
||||||
|
if drift > client_state.max_clock_drift {
|
||||||
|
return Err(IbcError::InvalidClientState("clock drift too large".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signatures (simplified - real implementation would verify BFT signatures)
|
||||||
|
self.verify_commit_signatures(client_state, header)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify commit signatures
|
||||||
|
fn verify_commit_signatures(
|
||||||
|
&self,
|
||||||
|
client_state: &ClientState,
|
||||||
|
header: &Header,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let commit = &header.signed_header.commit;
|
||||||
|
let validator_set = &header.validator_set;
|
||||||
|
|
||||||
|
// Calculate signed voting power
|
||||||
|
let mut signed_power = 0u64;
|
||||||
|
for sig in &commit.signatures {
|
||||||
|
if let Some(validator) = validator_set.get_by_address(&sig.validator_address) {
|
||||||
|
// In real implementation, verify signature here
|
||||||
|
signed_power += validator.voting_power;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check trust threshold
|
||||||
|
if !client_state
|
||||||
|
.trust_level
|
||||||
|
.check(signed_power, validator_set.total_voting_power)
|
||||||
|
{
|
||||||
|
return Err(IbcError::ProofVerificationFailed(
|
||||||
|
"insufficient voting power".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check misbehaviour and freeze client if detected
|
||||||
|
pub fn check_misbehaviour(
|
||||||
|
&mut self,
|
||||||
|
client_id: &ClientId,
|
||||||
|
header1: &Header,
|
||||||
|
header2: &Header,
|
||||||
|
) -> IbcResult<bool> {
|
||||||
|
// Misbehaviour: two different headers at the same height
|
||||||
|
if header1.height == header2.height
|
||||||
|
&& header1.signed_header.block_hash != header2.signed_header.block_hash
|
||||||
|
{
|
||||||
|
// Freeze the client
|
||||||
|
if let Some(state) = self.client_states.get_mut(client_id) {
|
||||||
|
state.freeze(header1.height);
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LightClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_id() {
|
||||||
|
let id = ClientId::new("07-tendermint", 0);
|
||||||
|
assert_eq!(id.to_string(), "07-tendermint-0");
|
||||||
|
assert_eq!(id.sequence(), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trust_level() {
|
||||||
|
let trust = TrustLevel::two_thirds();
|
||||||
|
assert!(trust.check(67, 100)); // 67% >= 66.67%
|
||||||
|
assert!(!trust.check(65, 100)); // 65% < 66.67%
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_state_validation() {
|
||||||
|
let mut state = ClientState::new_synor(
|
||||||
|
ChainId::synor_testnet(),
|
||||||
|
Height::from_height(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(state.validate().is_ok());
|
||||||
|
|
||||||
|
// Invalid: trusting period >= unbonding period
|
||||||
|
state.trusting_period = state.unbonding_period + 1;
|
||||||
|
assert!(state.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_light_client_create() {
|
||||||
|
let mut client = LightClient::new();
|
||||||
|
|
||||||
|
let state = ClientState::new_synor(ChainId::synor_testnet(), Height::from_height(100));
|
||||||
|
|
||||||
|
let consensus = ConsensusState::new(Timestamp::now(), vec![0u8; 32], vec![0u8; 32]);
|
||||||
|
|
||||||
|
let client_id = client.create_client(state, consensus).unwrap();
|
||||||
|
assert!(client.get_client_state(&client_id).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
374
crates/synor-ibc/src/commitment.rs
Normal file
374
crates/synor-ibc/src/commitment.rs
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
//! IBC Commitment and Proof Verification
|
||||||
|
//!
|
||||||
|
//! IBC uses Merkle proofs to verify state on remote chains.
|
||||||
|
//! This module handles commitment paths and proof verification.
|
||||||
|
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::channel::{ChannelId, PortId};
|
||||||
|
use crate::client::ClientId;
|
||||||
|
use crate::connection::ConnectionId;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
/// Merkle proof for IBC commitments
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MerkleProof {
|
||||||
|
/// Proof operations (inner nodes)
|
||||||
|
pub proofs: Vec<ProofOp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MerkleProof {
|
||||||
|
/// Create a new Merkle proof
|
||||||
|
pub fn new(proofs: Vec<ProofOp>) -> Self {
|
||||||
|
Self { proofs }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an empty proof (for testing)
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self { proofs: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the proof against a root and value
|
||||||
|
pub fn verify(&self, root: &[u8], _path: &MerklePath, value: &[u8]) -> IbcResult<bool> {
|
||||||
|
if self.proofs.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("empty proof".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute expected hash
|
||||||
|
let leaf_hash = hash_leaf(value);
|
||||||
|
let mut computed_hash = leaf_hash;
|
||||||
|
|
||||||
|
// Apply proof operations
|
||||||
|
for (i, op) in self.proofs.iter().enumerate() {
|
||||||
|
computed_hash = match &op.op_type {
|
||||||
|
ProofOpType::InnerHash { prefix, suffix } => {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(prefix);
|
||||||
|
hasher.update(&computed_hash);
|
||||||
|
hasher.update(suffix);
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
ProofOpType::LeafHash { prefix } => {
|
||||||
|
if i != 0 {
|
||||||
|
return Err(IbcError::ProofVerificationFailed(
|
||||||
|
"leaf hash must be first".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(prefix);
|
||||||
|
hasher.update(value);
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare with root
|
||||||
|
Ok(computed_hash == root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proof operation type
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ProofOpType {
|
||||||
|
/// Inner node hash
|
||||||
|
InnerHash {
|
||||||
|
/// Prefix bytes
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
/// Suffix bytes
|
||||||
|
suffix: Vec<u8>,
|
||||||
|
},
|
||||||
|
/// Leaf hash
|
||||||
|
LeafHash {
|
||||||
|
/// Prefix bytes
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single proof operation
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProofOp {
|
||||||
|
/// Key at this level
|
||||||
|
pub key: Vec<u8>,
|
||||||
|
/// Operation type
|
||||||
|
pub op_type: ProofOpType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProofOp {
|
||||||
|
/// Create an inner hash operation
|
||||||
|
pub fn inner(key: Vec<u8>, prefix: Vec<u8>, suffix: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
op_type: ProofOpType::InnerHash { prefix, suffix },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a leaf hash operation
|
||||||
|
pub fn leaf(key: Vec<u8>, prefix: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
op_type: ProofOpType::LeafHash { prefix },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merkle path for key lookups
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MerklePath {
|
||||||
|
/// Path segments
|
||||||
|
pub key_path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MerklePath {
|
||||||
|
/// Create a new Merkle path
|
||||||
|
pub fn new(segments: Vec<String>) -> Self {
|
||||||
|
Self { key_path: segments }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from key prefix and key
|
||||||
|
pub fn from_prefix_key(prefix: &str, key: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
key_path: vec![prefix.to_string(), key.to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get full path string
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.key_path.join("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get path bytes for hashing
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
self.to_string().into_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commitment proof wrapper
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CommitmentProof {
|
||||||
|
/// Merkle proof
|
||||||
|
pub proof: MerkleProof,
|
||||||
|
/// Proof height
|
||||||
|
pub height: crate::types::Height,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommitmentProof {
|
||||||
|
/// Create a new commitment proof
|
||||||
|
pub fn new(proof: MerkleProof, height: crate::types::Height) -> Self {
|
||||||
|
Self { proof, height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the proof
|
||||||
|
pub fn verify(&self, root: &[u8], path: &MerklePath, value: &[u8]) -> IbcResult<bool> {
|
||||||
|
self.proof.verify(root, path, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash a leaf value
|
||||||
|
fn hash_leaf(value: &[u8]) -> Vec<u8> {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&[0x00]); // Leaf prefix
|
||||||
|
hasher.update(value);
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash an inner node (used in full Merkle tree verification)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn hash_inner(left: &[u8], right: &[u8]) -> Vec<u8> {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&[0x01]); // Inner prefix
|
||||||
|
hasher.update(left);
|
||||||
|
hasher.update(right);
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IBC Commitment Paths
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Client state commitment path
|
||||||
|
pub fn client_state_path(client_id: &ClientId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"clients".to_string(),
|
||||||
|
client_id.to_string(),
|
||||||
|
"clientState".to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consensus state commitment path
|
||||||
|
pub fn consensus_state_path(client_id: &ClientId, height: &crate::types::Height) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"clients".to_string(),
|
||||||
|
client_id.to_string(),
|
||||||
|
"consensusStates".to_string(),
|
||||||
|
height.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection commitment path
|
||||||
|
pub fn connection_path(conn_id: &ConnectionId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"connections".to_string(),
|
||||||
|
conn_id.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel commitment path
|
||||||
|
pub fn channel_path(port_id: &PortId, channel_id: &ChannelId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"channelEnds".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet commitment path
|
||||||
|
pub fn packet_commitment_path(
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"commitments".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
"sequences".to_string(),
|
||||||
|
sequence.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet receipt path (for unordered channels)
|
||||||
|
pub fn packet_receipt_path(
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"receipts".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
"sequences".to_string(),
|
||||||
|
sequence.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet acknowledgement path
|
||||||
|
pub fn packet_acknowledgement_path(
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"acks".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
"sequences".to_string(),
|
||||||
|
sequence.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next sequence send path
|
||||||
|
pub fn next_sequence_send_path(port_id: &PortId, channel_id: &ChannelId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"nextSequenceSend".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next sequence receive path
|
||||||
|
pub fn next_sequence_recv_path(port_id: &PortId, channel_id: &ChannelId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"nextSequenceRecv".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next sequence acknowledge path
|
||||||
|
pub fn next_sequence_ack_path(port_id: &PortId, channel_id: &ChannelId) -> MerklePath {
|
||||||
|
MerklePath::new(vec![
|
||||||
|
"nextSequenceAck".to_string(),
|
||||||
|
"ports".to_string(),
|
||||||
|
port_id.to_string(),
|
||||||
|
"channels".to_string(),
|
||||||
|
channel_id.to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merkle_path() {
|
||||||
|
let path = client_state_path(&ClientId::new("07-tendermint", 0));
|
||||||
|
assert_eq!(
|
||||||
|
path.to_string(),
|
||||||
|
"clients/07-tendermint-0/clientState"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_paths() {
|
||||||
|
let port = PortId::transfer();
|
||||||
|
let channel = ChannelId::new(0);
|
||||||
|
|
||||||
|
let commit_path = packet_commitment_path(&port, &channel, 1);
|
||||||
|
assert!(commit_path.to_string().contains("commitments"));
|
||||||
|
|
||||||
|
let receipt_path = packet_receipt_path(&port, &channel, 1);
|
||||||
|
assert!(receipt_path.to_string().contains("receipts"));
|
||||||
|
|
||||||
|
let ack_path = packet_acknowledgement_path(&port, &channel, 1);
|
||||||
|
assert!(ack_path.to_string().contains("acks"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_leaf() {
|
||||||
|
let value = b"test value";
|
||||||
|
let hash1 = hash_leaf(value);
|
||||||
|
let hash2 = hash_leaf(value);
|
||||||
|
|
||||||
|
// Same value should produce same hash
|
||||||
|
assert_eq!(hash1, hash2);
|
||||||
|
|
||||||
|
// Different value should produce different hash
|
||||||
|
let hash3 = hash_leaf(b"different");
|
||||||
|
assert_ne!(hash1, hash3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_inner() {
|
||||||
|
let left = vec![1, 2, 3];
|
||||||
|
let right = vec![4, 5, 6];
|
||||||
|
|
||||||
|
let hash1 = hash_inner(&left, &right);
|
||||||
|
let hash2 = hash_inner(&left, &right);
|
||||||
|
assert_eq!(hash1, hash2);
|
||||||
|
|
||||||
|
// Order matters
|
||||||
|
let hash3 = hash_inner(&right, &left);
|
||||||
|
assert_ne!(hash1, hash3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proof_op() {
|
||||||
|
let leaf = ProofOp::leaf(b"key".to_vec(), b"prefix".to_vec());
|
||||||
|
assert!(matches!(leaf.op_type, ProofOpType::LeafHash { .. }));
|
||||||
|
|
||||||
|
let inner = ProofOp::inner(b"key".to_vec(), b"prefix".to_vec(), b"suffix".to_vec());
|
||||||
|
assert!(matches!(inner.op_type, ProofOpType::InnerHash { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
599
crates/synor-ibc/src/connection.rs
Normal file
599
crates/synor-ibc/src/connection.rs
Normal file
|
|
@ -0,0 +1,599 @@
|
||||||
|
//! IBC Connection Management
|
||||||
|
//!
|
||||||
|
//! Connections are the foundational layer of IBC, establishing a verified
|
||||||
|
//! link between two chains through their light clients.
|
||||||
|
//!
|
||||||
|
//! # Connection Handshake (4-way)
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! Chain A Chain B
|
||||||
|
//! │ │
|
||||||
|
//! │──── ConnOpenInit ────────────────────────>│
|
||||||
|
//! │ (Propose connection) │
|
||||||
|
//! │ │
|
||||||
|
//! │<─────────────────── ConnOpenTry ──────────│
|
||||||
|
//! │ (Accept & propose) │
|
||||||
|
//! │ │
|
||||||
|
//! │──── ConnOpenAck ─────────────────────────>│
|
||||||
|
//! │ (Acknowledge) │
|
||||||
|
//! │ │
|
||||||
|
//! │<─────────────────── ConnOpenConfirm ──────│
|
||||||
|
//! │ (Confirm) │
|
||||||
|
//! │ │
|
||||||
|
//! │ CONNECTION OPEN │
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::client::ClientId;
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::types::{CommitmentPrefix, Height, Version};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Connection identifier
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectionId(pub String);
|
||||||
|
|
||||||
|
impl ConnectionId {
|
||||||
|
/// Create a new connection ID
|
||||||
|
pub fn new(sequence: u64) -> Self {
|
||||||
|
Self(format!("connection-{}", sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse sequence from ID
|
||||||
|
pub fn sequence(&self) -> Option<u64> {
|
||||||
|
self.0.strip_prefix("connection-")?.parse().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConnectionId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection state in the handshake lifecycle
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ConnectionState {
|
||||||
|
/// Uninitialized (default)
|
||||||
|
Uninitialized,
|
||||||
|
/// Init: Chain A has sent ConnOpenInit
|
||||||
|
Init,
|
||||||
|
/// TryOpen: Chain B has sent ConnOpenTry
|
||||||
|
TryOpen,
|
||||||
|
/// Open: Connection is fully established
|
||||||
|
Open,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionState {
|
||||||
|
/// Get state name
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ConnectionState::Uninitialized => "UNINITIALIZED",
|
||||||
|
ConnectionState::Init => "INIT",
|
||||||
|
ConnectionState::TryOpen => "TRYOPEN",
|
||||||
|
ConnectionState::Open => "OPEN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConnectionState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counterparty connection information
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Counterparty {
|
||||||
|
/// Counterparty client ID
|
||||||
|
pub client_id: ClientId,
|
||||||
|
/// Counterparty connection ID (may be empty during init)
|
||||||
|
pub connection_id: Option<ConnectionId>,
|
||||||
|
/// Commitment prefix for the counterparty's key-value store
|
||||||
|
pub prefix: CommitmentPrefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counterparty {
|
||||||
|
/// Create a new counterparty
|
||||||
|
pub fn new(
|
||||||
|
client_id: ClientId,
|
||||||
|
connection_id: Option<ConnectionId>,
|
||||||
|
prefix: CommitmentPrefix,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id,
|
||||||
|
connection_id,
|
||||||
|
prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the counterparty
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.client_id.0.is_empty() {
|
||||||
|
return Err(IbcError::InvalidCounterparty(
|
||||||
|
"client ID cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection end state
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectionEnd {
|
||||||
|
/// Connection state
|
||||||
|
pub state: ConnectionState,
|
||||||
|
/// Client ID associated with this connection
|
||||||
|
pub client_id: ClientId,
|
||||||
|
/// Counterparty chain information
|
||||||
|
pub counterparty: Counterparty,
|
||||||
|
/// Supported versions
|
||||||
|
pub versions: Vec<Version>,
|
||||||
|
/// Delay period for packet verification (in nanoseconds)
|
||||||
|
pub delay_period: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionEnd {
|
||||||
|
/// Create a new connection end in Init state
|
||||||
|
pub fn new_init(
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty: Counterparty,
|
||||||
|
versions: Vec<Version>,
|
||||||
|
delay_period: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
state: ConnectionState::Init,
|
||||||
|
client_id,
|
||||||
|
counterparty,
|
||||||
|
versions,
|
||||||
|
delay_period,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new connection end in TryOpen state
|
||||||
|
pub fn new_try_open(
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty: Counterparty,
|
||||||
|
versions: Vec<Version>,
|
||||||
|
delay_period: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
state: ConnectionState::TryOpen,
|
||||||
|
client_id,
|
||||||
|
counterparty,
|
||||||
|
versions,
|
||||||
|
delay_period,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if connection is open
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.state == ConnectionState::Open
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition to TryOpen state
|
||||||
|
pub fn set_try_open(&mut self) {
|
||||||
|
self.state = ConnectionState::TryOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition to Open state
|
||||||
|
pub fn set_open(&mut self) {
|
||||||
|
self.state = ConnectionState::Open;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set counterparty connection ID
|
||||||
|
pub fn set_counterparty_connection_id(&mut self, id: ConnectionId) {
|
||||||
|
self.counterparty.connection_id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the connection end
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.client_id.0.is_empty() {
|
||||||
|
return Err(IbcError::InvalidConnectionState {
|
||||||
|
expected: "non-empty client ID".to_string(),
|
||||||
|
actual: "empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.counterparty.validate()?;
|
||||||
|
|
||||||
|
if self.versions.is_empty() {
|
||||||
|
return Err(IbcError::ConnectionVersionMismatch(
|
||||||
|
"at least one version required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection manager
|
||||||
|
pub struct ConnectionManager {
|
||||||
|
/// Connections by ID
|
||||||
|
connections: HashMap<ConnectionId, ConnectionEnd>,
|
||||||
|
/// Client to connection mapping
|
||||||
|
client_connections: HashMap<ClientId, Vec<ConnectionId>>,
|
||||||
|
/// Next connection sequence
|
||||||
|
next_connection_sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionManager {
|
||||||
|
/// Create a new connection manager
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
connections: HashMap::new(),
|
||||||
|
client_connections: HashMap::new(),
|
||||||
|
next_connection_sequence: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate next connection ID
|
||||||
|
fn next_connection_id(&mut self) -> ConnectionId {
|
||||||
|
let id = ConnectionId::new(self.next_connection_sequence);
|
||||||
|
self.next_connection_sequence += 1;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a connection by ID
|
||||||
|
pub fn get_connection(&self, conn_id: &ConnectionId) -> IbcResult<&ConnectionEnd> {
|
||||||
|
self.connections
|
||||||
|
.get(conn_id)
|
||||||
|
.ok_or_else(|| IbcError::ConnectionNotFound(conn_id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable connection
|
||||||
|
fn get_connection_mut(&mut self, conn_id: &ConnectionId) -> IbcResult<&mut ConnectionEnd> {
|
||||||
|
self.connections
|
||||||
|
.get_mut(conn_id)
|
||||||
|
.ok_or_else(|| IbcError::ConnectionNotFound(conn_id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all connections for a client
|
||||||
|
pub fn get_client_connections(&self, client_id: &ClientId) -> Vec<ConnectionId> {
|
||||||
|
self.client_connections
|
||||||
|
.get(client_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection Open Init - Start the handshake
|
||||||
|
///
|
||||||
|
/// Chain A calls this to propose a connection to Chain B
|
||||||
|
pub fn conn_open_init(
|
||||||
|
&mut self,
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty: Counterparty,
|
||||||
|
version: Option<Version>,
|
||||||
|
delay_period: u64,
|
||||||
|
) -> IbcResult<ConnectionId> {
|
||||||
|
// Validate counterparty
|
||||||
|
counterparty.validate()?;
|
||||||
|
|
||||||
|
// Generate connection ID
|
||||||
|
let conn_id = self.next_connection_id();
|
||||||
|
|
||||||
|
// Default version if not specified
|
||||||
|
let versions = match version {
|
||||||
|
Some(v) => vec![v],
|
||||||
|
None => vec![Version::default_connection()],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create connection end
|
||||||
|
let connection = ConnectionEnd::new_init(client_id.clone(), counterparty, versions, delay_period);
|
||||||
|
connection.validate()?;
|
||||||
|
|
||||||
|
// Store connection
|
||||||
|
self.connections.insert(conn_id.clone(), connection);
|
||||||
|
self.client_connections
|
||||||
|
.entry(client_id)
|
||||||
|
.or_default()
|
||||||
|
.push(conn_id.clone());
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
connection_id = %conn_id,
|
||||||
|
"Connection init complete"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(conn_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection Open Try - Respond to Init
|
||||||
|
///
|
||||||
|
/// Chain B calls this after receiving proof of Chain A's ConnOpenInit
|
||||||
|
pub fn conn_open_try(
|
||||||
|
&mut self,
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty: Counterparty,
|
||||||
|
counterparty_versions: Vec<Version>,
|
||||||
|
delay_period: u64,
|
||||||
|
proof_init: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<ConnectionId> {
|
||||||
|
// Validate counterparty
|
||||||
|
counterparty.validate()?;
|
||||||
|
|
||||||
|
// Verify we have the counterparty's connection ID (clone to avoid borrow issues)
|
||||||
|
let counterparty_conn_id = counterparty
|
||||||
|
.connection_id
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
IbcError::InvalidCounterparty("counterparty connection ID required".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Verify proof of init (simplified - real implementation verifies Merkle proof)
|
||||||
|
if proof_init.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_init required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select compatible version
|
||||||
|
let versions = self.pick_version(&counterparty_versions)?;
|
||||||
|
|
||||||
|
// Generate our connection ID
|
||||||
|
let conn_id = self.next_connection_id();
|
||||||
|
|
||||||
|
// Create connection end in TryOpen state
|
||||||
|
let connection =
|
||||||
|
ConnectionEnd::new_try_open(client_id.clone(), counterparty, versions, delay_period);
|
||||||
|
connection.validate()?;
|
||||||
|
|
||||||
|
// Store connection
|
||||||
|
self.connections.insert(conn_id.clone(), connection);
|
||||||
|
self.client_connections
|
||||||
|
.entry(client_id)
|
||||||
|
.or_default()
|
||||||
|
.push(conn_id.clone());
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
connection_id = %conn_id,
|
||||||
|
counterparty = %counterparty_conn_id,
|
||||||
|
"Connection try open complete"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(conn_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection Open Ack - Acknowledge Try
|
||||||
|
///
|
||||||
|
/// Chain A calls this after receiving proof of Chain B's ConnOpenTry
|
||||||
|
pub fn conn_open_ack(
|
||||||
|
&mut self,
|
||||||
|
conn_id: &ConnectionId,
|
||||||
|
counterparty_conn_id: ConnectionId,
|
||||||
|
version: Version,
|
||||||
|
proof_try: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
// Get connection
|
||||||
|
let connection = self.get_connection_mut(conn_id)?;
|
||||||
|
|
||||||
|
// Verify state
|
||||||
|
if connection.state != ConnectionState::Init {
|
||||||
|
return Err(IbcError::InvalidConnectionState {
|
||||||
|
expected: ConnectionState::Init.to_string(),
|
||||||
|
actual: connection.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_try.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_try required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify version is compatible
|
||||||
|
if !connection.versions.iter().any(|v| v.identifier == version.identifier) {
|
||||||
|
return Err(IbcError::ConnectionVersionMismatch(format!(
|
||||||
|
"version {} not supported",
|
||||||
|
version.identifier
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection
|
||||||
|
connection.set_counterparty_connection_id(counterparty_conn_id.clone());
|
||||||
|
connection.versions = vec![version];
|
||||||
|
connection.set_open();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
connection_id = %conn_id,
|
||||||
|
counterparty = %counterparty_conn_id,
|
||||||
|
"Connection ack complete - connection OPEN"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection Open Confirm - Final confirmation
|
||||||
|
///
|
||||||
|
/// Chain B calls this after receiving proof of Chain A's ConnOpenAck
|
||||||
|
pub fn conn_open_confirm(
|
||||||
|
&mut self,
|
||||||
|
conn_id: &ConnectionId,
|
||||||
|
proof_ack: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
// Get connection
|
||||||
|
let connection = self.get_connection_mut(conn_id)?;
|
||||||
|
|
||||||
|
// Verify state
|
||||||
|
if connection.state != ConnectionState::TryOpen {
|
||||||
|
return Err(IbcError::InvalidConnectionState {
|
||||||
|
expected: ConnectionState::TryOpen.to_string(),
|
||||||
|
actual: connection.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_ack.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_ack required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state to Open
|
||||||
|
connection.set_open();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
connection_id = %conn_id,
|
||||||
|
"Connection confirm complete - connection OPEN"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick a compatible version from counterparty's versions
|
||||||
|
fn pick_version(&self, counterparty_versions: &[Version]) -> IbcResult<Vec<Version>> {
|
||||||
|
let our_version = Version::default_connection();
|
||||||
|
|
||||||
|
// Find matching version
|
||||||
|
for version in counterparty_versions {
|
||||||
|
if version.identifier == our_version.identifier {
|
||||||
|
// Check features overlap
|
||||||
|
let common_features: Vec<String> = version
|
||||||
|
.features
|
||||||
|
.iter()
|
||||||
|
.filter(|f| our_version.features.contains(f))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !common_features.is_empty() {
|
||||||
|
return Ok(vec![Version::new(&version.identifier, common_features)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(IbcError::ConnectionVersionMismatch(
|
||||||
|
"no compatible version found".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all connections
|
||||||
|
pub fn all_connections(&self) -> impl Iterator<Item = (&ConnectionId, &ConnectionEnd)> {
|
||||||
|
self.connections.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count connections
|
||||||
|
pub fn connection_count(&self) -> usize {
|
||||||
|
self.connections.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConnectionManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_counterparty() -> Counterparty {
|
||||||
|
Counterparty::new(
|
||||||
|
ClientId::new("07-tendermint", 0),
|
||||||
|
None,
|
||||||
|
CommitmentPrefix::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_id() {
|
||||||
|
let id = ConnectionId::new(5);
|
||||||
|
assert_eq!(id.to_string(), "connection-5");
|
||||||
|
assert_eq!(id.sequence(), Some(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_init() {
|
||||||
|
let mut manager = ConnectionManager::new();
|
||||||
|
let client_id = ClientId::new("synor", 0);
|
||||||
|
|
||||||
|
let conn_id = manager
|
||||||
|
.conn_open_init(client_id.clone(), test_counterparty(), None, 0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let conn = manager.get_connection(&conn_id).unwrap();
|
||||||
|
assert_eq!(conn.state, ConnectionState::Init);
|
||||||
|
assert_eq!(conn.client_id, client_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_handshake() {
|
||||||
|
let mut manager_a = ConnectionManager::new();
|
||||||
|
let mut manager_b = ConnectionManager::new();
|
||||||
|
|
||||||
|
let client_a = ClientId::new("synor", 0);
|
||||||
|
let client_b = ClientId::new("synor", 1);
|
||||||
|
|
||||||
|
// Chain A: Init
|
||||||
|
let conn_a = manager_a
|
||||||
|
.conn_open_init(
|
||||||
|
client_a.clone(),
|
||||||
|
Counterparty::new(client_b.clone(), None, CommitmentPrefix::default()),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Chain B: TryOpen
|
||||||
|
let counterparty_b = Counterparty::new(
|
||||||
|
client_a.clone(),
|
||||||
|
Some(conn_a.clone()),
|
||||||
|
CommitmentPrefix::default(),
|
||||||
|
);
|
||||||
|
let conn_b = manager_b
|
||||||
|
.conn_open_try(
|
||||||
|
client_b.clone(),
|
||||||
|
counterparty_b,
|
||||||
|
vec![Version::default_connection()],
|
||||||
|
0,
|
||||||
|
vec![1, 2, 3], // Mock proof
|
||||||
|
Height::from_height(100),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Chain A: Ack
|
||||||
|
manager_a
|
||||||
|
.conn_open_ack(
|
||||||
|
&conn_a,
|
||||||
|
conn_b.clone(),
|
||||||
|
Version::default_connection(),
|
||||||
|
vec![1, 2, 3], // Mock proof
|
||||||
|
Height::from_height(101),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify Chain A is open
|
||||||
|
let conn = manager_a.get_connection(&conn_a).unwrap();
|
||||||
|
assert_eq!(conn.state, ConnectionState::Open);
|
||||||
|
|
||||||
|
// Chain B: Confirm
|
||||||
|
manager_b
|
||||||
|
.conn_open_confirm(
|
||||||
|
&conn_b,
|
||||||
|
vec![1, 2, 3], // Mock proof
|
||||||
|
Height::from_height(102),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify Chain B is open
|
||||||
|
let conn = manager_b.get_connection(&conn_b).unwrap();
|
||||||
|
assert_eq!(conn.state, ConnectionState::Open);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_state_transition() {
|
||||||
|
let mut manager = ConnectionManager::new();
|
||||||
|
let client_id = ClientId::new("synor", 0);
|
||||||
|
|
||||||
|
let conn_id = manager
|
||||||
|
.conn_open_init(client_id, test_counterparty(), None, 0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to confirm without going through TryOpen
|
||||||
|
let result = manager.conn_open_confirm(
|
||||||
|
&conn_id,
|
||||||
|
vec![1, 2, 3],
|
||||||
|
Height::from_height(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
163
crates/synor-ibc/src/error.rs
Normal file
163
crates/synor-ibc/src/error.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
//! IBC error types
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Result type for IBC operations
|
||||||
|
pub type IbcResult<T> = Result<T, IbcError>;
|
||||||
|
|
||||||
|
/// IBC error types
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IbcError {
|
||||||
|
// Client errors
|
||||||
|
#[error("Client not found: {0}")]
|
||||||
|
ClientNotFound(String),
|
||||||
|
|
||||||
|
#[error("Client already exists: {0}")]
|
||||||
|
ClientAlreadyExists(String),
|
||||||
|
|
||||||
|
#[error("Invalid client state: {0}")]
|
||||||
|
InvalidClientState(String),
|
||||||
|
|
||||||
|
#[error("Client frozen: {0}")]
|
||||||
|
ClientFrozen(String),
|
||||||
|
|
||||||
|
#[error("Client expired: {0}")]
|
||||||
|
ClientExpired(String),
|
||||||
|
|
||||||
|
// Connection errors
|
||||||
|
#[error("Connection not found: {0}")]
|
||||||
|
ConnectionNotFound(String),
|
||||||
|
|
||||||
|
#[error("Connection already exists: {0}")]
|
||||||
|
ConnectionAlreadyExists(String),
|
||||||
|
|
||||||
|
#[error("Invalid connection state: expected {expected}, got {actual}")]
|
||||||
|
InvalidConnectionState { expected: String, actual: String },
|
||||||
|
|
||||||
|
#[error("Connection version mismatch: {0}")]
|
||||||
|
ConnectionVersionMismatch(String),
|
||||||
|
|
||||||
|
// Channel errors
|
||||||
|
#[error("Channel not found: port={port}, channel={channel}")]
|
||||||
|
ChannelNotFound { port: String, channel: String },
|
||||||
|
|
||||||
|
#[error("Channel already exists: port={port}, channel={channel}")]
|
||||||
|
ChannelAlreadyExists { port: String, channel: String },
|
||||||
|
|
||||||
|
#[error("Invalid channel state: expected {expected}, got {actual}")]
|
||||||
|
InvalidChannelState { expected: String, actual: String },
|
||||||
|
|
||||||
|
#[error("Port not bound: {0}")]
|
||||||
|
PortNotBound(String),
|
||||||
|
|
||||||
|
#[error("Port already bound: {0}")]
|
||||||
|
PortAlreadyBound(String),
|
||||||
|
|
||||||
|
// Packet errors
|
||||||
|
#[error("Packet timeout: height={height}, timestamp={timestamp}")]
|
||||||
|
PacketTimeout { height: u64, timestamp: u64 },
|
||||||
|
|
||||||
|
#[error("Packet already received: sequence={0}")]
|
||||||
|
PacketAlreadyReceived(u64),
|
||||||
|
|
||||||
|
#[error("Packet not found: sequence={0}")]
|
||||||
|
PacketNotFound(u64),
|
||||||
|
|
||||||
|
#[error("Invalid packet sequence: expected {expected}, got {actual}")]
|
||||||
|
InvalidPacketSequence { expected: u64, actual: u64 },
|
||||||
|
|
||||||
|
#[error("Packet data too large: {size} bytes (max {max})")]
|
||||||
|
PacketDataTooLarge { size: usize, max: usize },
|
||||||
|
|
||||||
|
#[error("Invalid packet data: {0}")]
|
||||||
|
InvalidPacketData(String),
|
||||||
|
|
||||||
|
#[error("Invalid acknowledgement: {0}")]
|
||||||
|
InvalidAcknowledgement(String),
|
||||||
|
|
||||||
|
// Swap errors
|
||||||
|
#[error("Swap not found: {0}")]
|
||||||
|
SwapNotFound(String),
|
||||||
|
|
||||||
|
#[error("Invalid swap state: expected {expected}, got {actual}")]
|
||||||
|
InvalidSwapState { expected: String, actual: String },
|
||||||
|
|
||||||
|
#[error("Swap timeout expired: {0}")]
|
||||||
|
SwapTimeoutExpired(String),
|
||||||
|
|
||||||
|
#[error("Invalid secret: {0}")]
|
||||||
|
InvalidSecret(String),
|
||||||
|
|
||||||
|
// Proof errors
|
||||||
|
#[error("Proof verification failed: {0}")]
|
||||||
|
ProofVerificationFailed(String),
|
||||||
|
|
||||||
|
#[error("Invalid proof: {0}")]
|
||||||
|
InvalidProof(String),
|
||||||
|
|
||||||
|
#[error("Invalid proof height: expected {expected}, got {actual}")]
|
||||||
|
InvalidProofHeight { expected: u64, actual: u64 },
|
||||||
|
|
||||||
|
#[error("Missing proof: {0}")]
|
||||||
|
MissingProof(String),
|
||||||
|
|
||||||
|
// Commitment errors
|
||||||
|
#[error("Commitment not found: {0}")]
|
||||||
|
CommitmentNotFound(String),
|
||||||
|
|
||||||
|
#[error("Invalid commitment: {0}")]
|
||||||
|
InvalidCommitment(String),
|
||||||
|
|
||||||
|
// Handshake errors
|
||||||
|
#[error("Handshake error: {0}")]
|
||||||
|
HandshakeError(String),
|
||||||
|
|
||||||
|
#[error("Invalid counterparty: {0}")]
|
||||||
|
InvalidCounterparty(String),
|
||||||
|
|
||||||
|
// Serialization errors
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
SerializationError(String),
|
||||||
|
|
||||||
|
#[error("Deserialization error: {0}")]
|
||||||
|
DeserializationError(String),
|
||||||
|
|
||||||
|
// General errors
|
||||||
|
#[error("Invalid identifier: {0}")]
|
||||||
|
InvalidIdentifier(String),
|
||||||
|
|
||||||
|
#[error("Unauthorized: {0}")]
|
||||||
|
Unauthorized(String),
|
||||||
|
|
||||||
|
#[error("Internal error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for IbcError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
IbcError::SerializationError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for IbcError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
IbcError::Internal(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_display() {
|
||||||
|
let err = IbcError::ClientNotFound("client-1".to_string());
|
||||||
|
assert!(err.to_string().contains("client-1"));
|
||||||
|
|
||||||
|
let err = IbcError::PacketTimeout {
|
||||||
|
height: 100,
|
||||||
|
timestamp: 1000,
|
||||||
|
};
|
||||||
|
assert!(err.to_string().contains("100"));
|
||||||
|
}
|
||||||
|
}
|
||||||
686
crates/synor-ibc/src/handler.rs
Normal file
686
crates/synor-ibc/src/handler.rs
Normal file
|
|
@ -0,0 +1,686 @@
|
||||||
|
//! IBC Handler - Main interface for IBC operations
|
||||||
|
//!
|
||||||
|
//! The handler coordinates light clients, connections, channels, and packets.
|
||||||
|
|
||||||
|
use crate::channel::{Channel, ChannelId, ChannelManager, ChannelOrder, PortId};
|
||||||
|
use crate::client::{ClientId, ClientState, ConsensusState, Header, LightClient};
|
||||||
|
use crate::connection::{ConnectionEnd, ConnectionId, ConnectionManager, Counterparty};
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::packet::{Acknowledgement, FungibleTokenPacketData, Packet, PacketHandler, Timeout};
|
||||||
|
use crate::types::{Height, Timestamp, Version};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// IBC handler configuration
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IbcConfig {
|
||||||
|
/// Chain ID
|
||||||
|
pub chain_id: String,
|
||||||
|
/// Default connection delay period (nanoseconds)
|
||||||
|
pub default_delay_period: u64,
|
||||||
|
/// Enable packet timeout
|
||||||
|
pub enable_timeout: bool,
|
||||||
|
/// Maximum packet data size
|
||||||
|
pub max_packet_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IbcConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
chain_id: "synor-1".to_string(),
|
||||||
|
default_delay_period: 0,
|
||||||
|
enable_timeout: true,
|
||||||
|
max_packet_size: 1024 * 1024, // 1 MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main IBC handler
|
||||||
|
pub struct IbcHandler {
|
||||||
|
/// Configuration
|
||||||
|
config: IbcConfig,
|
||||||
|
/// Light client manager
|
||||||
|
light_client: Arc<RwLock<LightClient>>,
|
||||||
|
/// Connection manager
|
||||||
|
connections: Arc<RwLock<ConnectionManager>>,
|
||||||
|
/// Channel manager
|
||||||
|
channels: Arc<RwLock<ChannelManager>>,
|
||||||
|
/// Packet handler
|
||||||
|
packets: Arc<RwLock<PacketHandler>>,
|
||||||
|
/// Current block height
|
||||||
|
current_height: Arc<RwLock<Height>>,
|
||||||
|
/// Current block timestamp
|
||||||
|
current_timestamp: Arc<RwLock<Timestamp>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IbcHandler {
|
||||||
|
/// Create a new IBC handler
|
||||||
|
pub fn new(config: IbcConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
light_client: Arc::new(RwLock::new(LightClient::new())),
|
||||||
|
connections: Arc::new(RwLock::new(ConnectionManager::new())),
|
||||||
|
channels: Arc::new(RwLock::new(ChannelManager::new())),
|
||||||
|
packets: Arc::new(RwLock::new(PacketHandler::new())),
|
||||||
|
current_height: Arc::new(RwLock::new(Height::from_height(1))),
|
||||||
|
current_timestamp: Arc::new(RwLock::new(Timestamp::now())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update current block height and timestamp
|
||||||
|
pub fn update_block(&self, height: Height, timestamp: Timestamp) {
|
||||||
|
*self.current_height.write() = height;
|
||||||
|
*self.current_timestamp.write() = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current height
|
||||||
|
pub fn current_height(&self) -> Height {
|
||||||
|
*self.current_height.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp
|
||||||
|
pub fn current_timestamp(&self) -> Timestamp {
|
||||||
|
*self.current_timestamp.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Client Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Create a new light client
|
||||||
|
pub fn create_client(
|
||||||
|
&self,
|
||||||
|
client_state: ClientState,
|
||||||
|
consensus_state: ConsensusState,
|
||||||
|
) -> IbcResult<ClientId> {
|
||||||
|
let mut client = self.light_client.write();
|
||||||
|
let client_id = client.create_client(client_state, consensus_state)?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
client_id = %client_id,
|
||||||
|
"Created IBC client"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(client_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a light client with a new header
|
||||||
|
pub fn update_client(&self, client_id: &ClientId, header: Header) -> IbcResult<()> {
|
||||||
|
let mut client = self.light_client.write();
|
||||||
|
client.update_client(client_id, header)?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
client_id = %client_id,
|
||||||
|
"Updated IBC client"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get client state
|
||||||
|
pub fn get_client_state(&self, client_id: &ClientId) -> IbcResult<ClientState> {
|
||||||
|
let client = self.light_client.read();
|
||||||
|
Ok(client.get_client_state(client_id)?.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Connection Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Initialize a connection (ConnOpenInit)
|
||||||
|
pub fn connection_open_init(
|
||||||
|
&self,
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty_client_id: ClientId,
|
||||||
|
version: Option<Version>,
|
||||||
|
) -> IbcResult<ConnectionId> {
|
||||||
|
// Verify client exists
|
||||||
|
let _ = self.get_client_state(&client_id)?;
|
||||||
|
|
||||||
|
let counterparty = Counterparty::new(
|
||||||
|
counterparty_client_id,
|
||||||
|
None,
|
||||||
|
crate::types::CommitmentPrefix::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut connections = self.connections.write();
|
||||||
|
connections.conn_open_init(
|
||||||
|
client_id,
|
||||||
|
counterparty,
|
||||||
|
version,
|
||||||
|
self.config.default_delay_period,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to open a connection (ConnOpenTry)
|
||||||
|
pub fn connection_open_try(
|
||||||
|
&self,
|
||||||
|
client_id: ClientId,
|
||||||
|
counterparty_client_id: ClientId,
|
||||||
|
counterparty_conn_id: ConnectionId,
|
||||||
|
counterparty_versions: Vec<Version>,
|
||||||
|
proof_init: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<ConnectionId> {
|
||||||
|
// Verify client exists
|
||||||
|
let _ = self.get_client_state(&client_id)?;
|
||||||
|
|
||||||
|
let counterparty = Counterparty::new(
|
||||||
|
counterparty_client_id,
|
||||||
|
Some(counterparty_conn_id),
|
||||||
|
crate::types::CommitmentPrefix::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut connections = self.connections.write();
|
||||||
|
connections.conn_open_try(
|
||||||
|
client_id,
|
||||||
|
counterparty,
|
||||||
|
counterparty_versions,
|
||||||
|
self.config.default_delay_period,
|
||||||
|
proof_init,
|
||||||
|
proof_height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acknowledge a connection (ConnOpenAck)
|
||||||
|
pub fn connection_open_ack(
|
||||||
|
&self,
|
||||||
|
conn_id: &ConnectionId,
|
||||||
|
counterparty_conn_id: ConnectionId,
|
||||||
|
version: Version,
|
||||||
|
proof_try: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let mut connections = self.connections.write();
|
||||||
|
connections.conn_open_ack(conn_id, counterparty_conn_id, version, proof_try, proof_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm a connection (ConnOpenConfirm)
|
||||||
|
pub fn connection_open_confirm(
|
||||||
|
&self,
|
||||||
|
conn_id: &ConnectionId,
|
||||||
|
proof_ack: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let mut connections = self.connections.write();
|
||||||
|
connections.conn_open_confirm(conn_id, proof_ack, proof_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get connection
|
||||||
|
pub fn get_connection(&self, conn_id: &ConnectionId) -> IbcResult<ConnectionEnd> {
|
||||||
|
let connections = self.connections.read();
|
||||||
|
Ok(connections.get_connection(conn_id)?.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Channel Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Bind a port to a module
|
||||||
|
pub fn bind_port(&self, port_id: PortId, module: String) -> IbcResult<()> {
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.bind_port(port_id, module)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a channel (ChanOpenInit)
|
||||||
|
pub fn channel_open_init(
|
||||||
|
&self,
|
||||||
|
port_id: PortId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
counterparty_port: PortId,
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
version: String,
|
||||||
|
) -> IbcResult<ChannelId> {
|
||||||
|
// Verify connection exists and is open
|
||||||
|
let connection = self.get_connection(&connection_id)?;
|
||||||
|
if !connection.is_open() {
|
||||||
|
return Err(IbcError::InvalidConnectionState {
|
||||||
|
expected: "OPEN".to_string(),
|
||||||
|
actual: connection.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.chan_open_init(
|
||||||
|
port_id,
|
||||||
|
ordering,
|
||||||
|
vec![connection_id],
|
||||||
|
counterparty_port,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to open a channel (ChanOpenTry)
|
||||||
|
pub fn channel_open_try(
|
||||||
|
&self,
|
||||||
|
port_id: PortId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
counterparty_port: PortId,
|
||||||
|
counterparty_channel: ChannelId,
|
||||||
|
ordering: ChannelOrder,
|
||||||
|
version: String,
|
||||||
|
counterparty_version: String,
|
||||||
|
proof_init: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<ChannelId> {
|
||||||
|
// Verify connection
|
||||||
|
let connection = self.get_connection(&connection_id)?;
|
||||||
|
if !connection.is_open() {
|
||||||
|
return Err(IbcError::InvalidConnectionState {
|
||||||
|
expected: "OPEN".to_string(),
|
||||||
|
actual: connection.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.chan_open_try(
|
||||||
|
port_id,
|
||||||
|
ordering,
|
||||||
|
vec![connection_id],
|
||||||
|
counterparty_port,
|
||||||
|
counterparty_channel,
|
||||||
|
version,
|
||||||
|
counterparty_version,
|
||||||
|
proof_init,
|
||||||
|
proof_height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acknowledge a channel (ChanOpenAck)
|
||||||
|
pub fn channel_open_ack(
|
||||||
|
&self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
counterparty_channel: ChannelId,
|
||||||
|
counterparty_version: String,
|
||||||
|
proof_try: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.chan_open_ack(
|
||||||
|
port_id,
|
||||||
|
channel_id,
|
||||||
|
counterparty_channel,
|
||||||
|
counterparty_version,
|
||||||
|
proof_try,
|
||||||
|
proof_height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm a channel (ChanOpenConfirm)
|
||||||
|
pub fn channel_open_confirm(
|
||||||
|
&self,
|
||||||
|
port_id: &PortId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
proof_ack: Vec<u8>,
|
||||||
|
proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.chan_open_confirm(port_id, channel_id, proof_ack, proof_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a channel
|
||||||
|
pub fn channel_close_init(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<()> {
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
channels.chan_close_init(port_id, channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get channel
|
||||||
|
pub fn get_channel(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult<Channel> {
|
||||||
|
let channels = self.channels.read();
|
||||||
|
Ok(channels.get_channel(port_id, channel_id)?.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Packet Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Send a packet
|
||||||
|
pub fn send_packet(
|
||||||
|
&self,
|
||||||
|
source_port: PortId,
|
||||||
|
source_channel: ChannelId,
|
||||||
|
data: Vec<u8>,
|
||||||
|
timeout: Timeout,
|
||||||
|
) -> IbcResult<u64> {
|
||||||
|
// Validate data size
|
||||||
|
if data.len() > self.config.max_packet_size {
|
||||||
|
return Err(IbcError::PacketDataTooLarge {
|
||||||
|
size: data.len(),
|
||||||
|
max: self.config.max_packet_size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get channel and verify it's open
|
||||||
|
let channel = self.get_channel(&source_port, &source_channel)?;
|
||||||
|
if !channel.is_open() {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: "OPEN".to_string(),
|
||||||
|
actual: channel.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let counterparty = &channel.counterparty;
|
||||||
|
let dest_port = counterparty.port_id.clone();
|
||||||
|
let dest_channel = counterparty.channel_id.clone().ok_or_else(|| {
|
||||||
|
IbcError::InvalidChannelState {
|
||||||
|
expected: "counterparty channel set".to_string(),
|
||||||
|
actual: "none".to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get sequence number
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
let sequence = channels.increment_send_sequence(&source_port, &source_channel);
|
||||||
|
|
||||||
|
// Create packet
|
||||||
|
let packet = Packet::new(
|
||||||
|
sequence,
|
||||||
|
source_port.clone(),
|
||||||
|
source_channel.clone(),
|
||||||
|
dest_port,
|
||||||
|
dest_channel,
|
||||||
|
data,
|
||||||
|
timeout.height,
|
||||||
|
timeout.timestamp,
|
||||||
|
);
|
||||||
|
packet.validate()?;
|
||||||
|
|
||||||
|
// Store commitment
|
||||||
|
let mut packets = self.packets.write();
|
||||||
|
packets.store_packet_commitment(&packet);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %source_port,
|
||||||
|
channel = %source_channel,
|
||||||
|
sequence = sequence,
|
||||||
|
"Packet sent"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive a packet
|
||||||
|
pub fn recv_packet(
|
||||||
|
&self,
|
||||||
|
packet: Packet,
|
||||||
|
proof_commitment: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<Acknowledgement> {
|
||||||
|
// Validate packet
|
||||||
|
packet.validate()?;
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
let current_height = self.current_height();
|
||||||
|
let current_time = self.current_timestamp();
|
||||||
|
if packet.is_timed_out(current_height, current_time) {
|
||||||
|
return Err(IbcError::PacketTimeout {
|
||||||
|
height: current_height.revision_height,
|
||||||
|
timestamp: current_time.nanoseconds(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get channel and verify
|
||||||
|
let channel = self.get_channel(&packet.dest_port, &packet.dest_channel)?;
|
||||||
|
if !channel.is_open() {
|
||||||
|
return Err(IbcError::InvalidChannelState {
|
||||||
|
expected: "OPEN".to_string(),
|
||||||
|
actual: channel.state.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_commitment.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_commitment required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle based on channel ordering
|
||||||
|
let mut packets = self.packets.write();
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
|
||||||
|
match channel.ordering {
|
||||||
|
ChannelOrder::Unordered => {
|
||||||
|
// Verify no receipt exists
|
||||||
|
packets.verify_no_receipt(&packet)?;
|
||||||
|
}
|
||||||
|
ChannelOrder::Ordered => {
|
||||||
|
// Verify sequence
|
||||||
|
let expected = channels.get_sequences(&packet.dest_port, &packet.dest_channel).next_recv;
|
||||||
|
packets.verify_ordered_sequence(&packet, expected)?;
|
||||||
|
channels.increment_recv_sequence(&packet.dest_port, &packet.dest_channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store receipt
|
||||||
|
packets.store_packet_receipt(&packet, current_height, None);
|
||||||
|
|
||||||
|
// Generate acknowledgement (success)
|
||||||
|
let ack = Acknowledgement::success(vec![]);
|
||||||
|
|
||||||
|
// Store ack commitment
|
||||||
|
packets.store_ack_commitment(
|
||||||
|
&packet.dest_port,
|
||||||
|
&packet.dest_channel,
|
||||||
|
packet.sequence,
|
||||||
|
&ack,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %packet.dest_port,
|
||||||
|
channel = %packet.dest_channel,
|
||||||
|
sequence = packet.sequence,
|
||||||
|
"Packet received"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acknowledge a packet
|
||||||
|
pub fn acknowledge_packet(
|
||||||
|
&self,
|
||||||
|
packet: Packet,
|
||||||
|
acknowledgement: Acknowledgement,
|
||||||
|
proof_ack: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
// Get channel
|
||||||
|
let channel = self.get_channel(&packet.source_port, &packet.source_channel)?;
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_ack.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_ack required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packets = self.packets.write();
|
||||||
|
|
||||||
|
// Verify commitment exists
|
||||||
|
if packets
|
||||||
|
.get_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err(IbcError::PacketNotFound(packet.sequence));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ordered channels, verify sequence
|
||||||
|
if channel.ordering == ChannelOrder::Ordered {
|
||||||
|
let mut channels = self.channels.write();
|
||||||
|
let expected = channels.get_sequences(&packet.source_port, &packet.source_channel).next_ack;
|
||||||
|
if packet.sequence != expected {
|
||||||
|
return Err(IbcError::InvalidPacketSequence {
|
||||||
|
expected,
|
||||||
|
actual: packet.sequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
channels.increment_ack_sequence(&packet.source_port, &packet.source_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete commitment
|
||||||
|
packets.delete_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %packet.source_port,
|
||||||
|
channel = %packet.source_channel,
|
||||||
|
sequence = packet.sequence,
|
||||||
|
success = acknowledgement.is_success(),
|
||||||
|
"Packet acknowledged"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout a packet
|
||||||
|
pub fn timeout_packet(
|
||||||
|
&self,
|
||||||
|
packet: Packet,
|
||||||
|
proof_unreceived: Vec<u8>,
|
||||||
|
_proof_height: Height,
|
||||||
|
_next_sequence_recv: u64,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
// Verify the packet timed out
|
||||||
|
let _current_height = self.current_height();
|
||||||
|
let _current_time = self.current_timestamp();
|
||||||
|
|
||||||
|
// Note: For timeout, we check if DESTINATION chain's height/time exceeds timeout
|
||||||
|
// This is a simplification - real implementation would verify against proof_height
|
||||||
|
|
||||||
|
// Verify proof (simplified)
|
||||||
|
if proof_unreceived.is_empty() {
|
||||||
|
return Err(IbcError::MissingProof("proof_unreceived required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packets = self.packets.write();
|
||||||
|
packets.timeout_packet(&packet)?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
port = %packet.source_port,
|
||||||
|
channel = %packet.source_channel,
|
||||||
|
sequence = packet.sequence,
|
||||||
|
"Packet timed out"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Token Transfer (ICS-20)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Send tokens via IBC transfer
|
||||||
|
pub fn transfer(
|
||||||
|
&self,
|
||||||
|
source_port: PortId,
|
||||||
|
source_channel: ChannelId,
|
||||||
|
denom: String,
|
||||||
|
amount: String,
|
||||||
|
sender: String,
|
||||||
|
receiver: String,
|
||||||
|
timeout: Timeout,
|
||||||
|
) -> IbcResult<u64> {
|
||||||
|
let packet_data = FungibleTokenPacketData::new(denom, amount, sender, receiver);
|
||||||
|
let data = packet_data.encode();
|
||||||
|
|
||||||
|
self.send_packet(source_port, source_channel, data, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Stats and Info
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Get connection count
|
||||||
|
pub fn connection_count(&self) -> usize {
|
||||||
|
self.connections.read().connection_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get chain ID
|
||||||
|
pub fn chain_id(&self) -> &str {
|
||||||
|
&self.config.chain_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IbcHandler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(IbcConfig::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::client::TrustLevel;
|
||||||
|
use crate::types::ChainId;
|
||||||
|
|
||||||
|
fn setup_handler() -> IbcHandler {
|
||||||
|
let handler = IbcHandler::new(IbcConfig::default());
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
let client_state = ClientState::new_synor(
|
||||||
|
ChainId::synor_testnet(),
|
||||||
|
Height::from_height(100),
|
||||||
|
);
|
||||||
|
let consensus_state = ConsensusState::new(
|
||||||
|
Timestamp::now(),
|
||||||
|
vec![0u8; 32],
|
||||||
|
vec![0u8; 32],
|
||||||
|
);
|
||||||
|
|
||||||
|
handler.create_client(client_state, consensus_state).unwrap();
|
||||||
|
handler.bind_port(PortId::transfer(), "transfer".to_string()).unwrap();
|
||||||
|
|
||||||
|
handler
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_handler_creation() {
|
||||||
|
let handler = IbcHandler::default();
|
||||||
|
assert_eq!(handler.chain_id(), "synor-1");
|
||||||
|
assert_eq!(handler.connection_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_creation() {
|
||||||
|
let handler = IbcHandler::default();
|
||||||
|
|
||||||
|
let client_state = ClientState::new_synor(
|
||||||
|
ChainId::synor_testnet(),
|
||||||
|
Height::from_height(100),
|
||||||
|
);
|
||||||
|
let consensus_state = ConsensusState::new(
|
||||||
|
Timestamp::now(),
|
||||||
|
vec![0u8; 32],
|
||||||
|
vec![0u8; 32],
|
||||||
|
);
|
||||||
|
|
||||||
|
let client_id = handler.create_client(client_state, consensus_state).unwrap();
|
||||||
|
assert!(handler.get_client_state(&client_id).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_init() {
|
||||||
|
let handler = setup_handler();
|
||||||
|
|
||||||
|
let client_id = ClientId::new("synor", 0);
|
||||||
|
let counterparty_client = ClientId::new("07-tendermint", 0);
|
||||||
|
|
||||||
|
let conn_id = handler.connection_open_init(
|
||||||
|
client_id,
|
||||||
|
counterparty_client,
|
||||||
|
None,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let conn = handler.get_connection(&conn_id).unwrap();
|
||||||
|
assert!(!conn.is_open()); // Still in Init state
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_block() {
|
||||||
|
let handler = IbcHandler::default();
|
||||||
|
|
||||||
|
let new_height = Height::from_height(100);
|
||||||
|
let new_time = Timestamp::from_seconds(1000);
|
||||||
|
|
||||||
|
handler.update_block(new_height, new_time);
|
||||||
|
|
||||||
|
assert_eq!(handler.current_height(), new_height);
|
||||||
|
assert_eq!(handler.current_timestamp(), new_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
crates/synor-ibc/src/lib.rs
Normal file
132
crates/synor-ibc/src/lib.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
//! Synor IBC - Inter-Blockchain Communication Protocol
|
||||||
|
//!
|
||||||
|
//! This crate implements the IBC protocol for cross-chain communication,
|
||||||
|
//! enabling Synor to interoperate with Cosmos SDK chains and other IBC-compatible blockchains.
|
||||||
|
//!
|
||||||
|
//! # Architecture
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! ┌─────────────────────────────────────────────────────────────┐
|
||||||
|
//! │ Synor Blockchain │
|
||||||
|
//! ├─────────────────────────────────────────────────────────────┤
|
||||||
|
//! │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
//! │ │ IBC Client │ │ Connection │ │ Channel │ │
|
||||||
|
//! │ │ (Light │ │ (Verified │ │ (Ordered/Unordered │ │
|
||||||
|
//! │ │ Client) │ │ Link) │ │ Message Passing) │ │
|
||||||
|
//! │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||||
|
//! │ │ │ │ │
|
||||||
|
//! │ └────────────────┼─────────────────────┘ │
|
||||||
|
//! │ │ │
|
||||||
|
//! │ ┌─────┴─────┐ │
|
||||||
|
//! │ │ Packet │ │
|
||||||
|
//! │ │ Handler │ │
|
||||||
|
//! │ └─────┬─────┘ │
|
||||||
|
//! └──────────────────────────┼──────────────────────────────────┘
|
||||||
|
//! │
|
||||||
|
//! ┌─────┴─────┐
|
||||||
|
//! │ Relayer │
|
||||||
|
//! └─────┬─────┘
|
||||||
|
//! │
|
||||||
|
//! ┌──────────────────────────┼──────────────────────────────────┐
|
||||||
|
//! │ Remote Chain │
|
||||||
|
//! └─────────────────────────────────────────────────────────────┘
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # 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
|
||||||
|
//!
|
||||||
|
//! ## Channel Handshake (4-way)
|
||||||
|
//! 1. `ChanOpenInit` - Chain A initiates
|
||||||
|
//! 2. `ChanOpenTry` - Chain B responds
|
||||||
|
//! 3. `ChanOpenAck` - Chain A acknowledges
|
||||||
|
//! 4. `ChanOpenConfirm` - Chain B confirms
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! use synor_ibc::{IbcHandler, ConnectionId, ChannelId};
|
||||||
|
//!
|
||||||
|
//! // Initialize IBC handler
|
||||||
|
//! let handler = IbcHandler::new(config);
|
||||||
|
//!
|
||||||
|
//! // Create a connection to a remote chain
|
||||||
|
//! let conn_id = handler.connection_open_init(
|
||||||
|
//! client_id,
|
||||||
|
//! counterparty,
|
||||||
|
//! version,
|
||||||
|
//! ).await?;
|
||||||
|
//!
|
||||||
|
//! // Open a channel for token transfers
|
||||||
|
//! let channel_id = handler.channel_open_init(
|
||||||
|
//! port_id,
|
||||||
|
//! conn_id,
|
||||||
|
//! counterparty_port,
|
||||||
|
//! version,
|
||||||
|
//! ).await?;
|
||||||
|
//!
|
||||||
|
//! // Send a packet
|
||||||
|
//! handler.send_packet(
|
||||||
|
//! channel_id,
|
||||||
|
//! data,
|
||||||
|
//! timeout,
|
||||||
|
//! ).await?;
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub mod connection;
|
||||||
|
pub mod channel;
|
||||||
|
pub mod packet;
|
||||||
|
pub mod commitment;
|
||||||
|
pub mod error;
|
||||||
|
pub mod handler;
|
||||||
|
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};
|
||||||
|
pub use error::{IbcError, IbcResult};
|
||||||
|
pub use handler::IbcHandler;
|
||||||
|
pub use swap::{AtomicSwap, Htlc, SwapManager, SwapId, SwapState, SwapAsset, Hashlock, Timelock};
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
|
/// IBC protocol version
|
||||||
|
pub const IBC_VERSION: &str = "1";
|
||||||
|
|
||||||
|
/// Default packet timeout in blocks
|
||||||
|
pub const DEFAULT_TIMEOUT_HEIGHT: u64 = 1000;
|
||||||
|
|
||||||
|
/// Default packet timeout in nanoseconds (1 hour)
|
||||||
|
pub const DEFAULT_TIMEOUT_TIMESTAMP: 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";
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
566
crates/synor-ibc/src/packet.rs
Normal file
566
crates/synor-ibc/src/packet.rs
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
//! IBC Packet Handling
|
||||||
|
//!
|
||||||
|
//! Packets are the fundamental unit of data transfer in IBC.
|
||||||
|
//! Each packet contains application data and routing information.
|
||||||
|
|
||||||
|
use crate::channel::{ChannelId, PortId};
|
||||||
|
use crate::error::{IbcError, IbcResult};
|
||||||
|
use crate::types::{Height, Timestamp};
|
||||||
|
use crate::MAX_PACKET_DATA_SIZE;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// IBC Packet
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Packet {
|
||||||
|
/// Packet sequence number
|
||||||
|
pub sequence: u64,
|
||||||
|
/// Source port
|
||||||
|
pub source_port: PortId,
|
||||||
|
/// Source channel
|
||||||
|
pub source_channel: ChannelId,
|
||||||
|
/// Destination port
|
||||||
|
pub dest_port: PortId,
|
||||||
|
/// Destination channel
|
||||||
|
pub dest_channel: ChannelId,
|
||||||
|
/// Packet data (app-specific)
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
/// Timeout height (0 = disabled)
|
||||||
|
pub timeout_height: Height,
|
||||||
|
/// Timeout timestamp in nanoseconds (0 = disabled)
|
||||||
|
pub timeout_timestamp: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packet {
|
||||||
|
/// Create a new packet
|
||||||
|
pub fn new(
|
||||||
|
sequence: u64,
|
||||||
|
source_port: PortId,
|
||||||
|
source_channel: ChannelId,
|
||||||
|
dest_port: PortId,
|
||||||
|
dest_channel: ChannelId,
|
||||||
|
data: Vec<u8>,
|
||||||
|
timeout_height: Height,
|
||||||
|
timeout_timestamp: Timestamp,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
sequence,
|
||||||
|
source_port,
|
||||||
|
source_channel,
|
||||||
|
dest_port,
|
||||||
|
dest_channel,
|
||||||
|
data,
|
||||||
|
timeout_height,
|
||||||
|
timeout_timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if packet has timed out based on height
|
||||||
|
pub fn is_timed_out_by_height(&self, current_height: Height) -> bool {
|
||||||
|
!self.timeout_height.is_zero() && current_height >= self.timeout_height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if packet has timed out based on timestamp
|
||||||
|
pub fn is_timed_out_by_timestamp(&self, current_time: Timestamp) -> bool {
|
||||||
|
!self.timeout_timestamp.is_zero() && current_time >= self.timeout_timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if packet has timed out
|
||||||
|
pub fn is_timed_out(&self, current_height: Height, current_time: Timestamp) -> bool {
|
||||||
|
self.is_timed_out_by_height(current_height) || self.is_timed_out_by_timestamp(current_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the packet
|
||||||
|
pub fn validate(&self) -> IbcResult<()> {
|
||||||
|
if self.sequence == 0 {
|
||||||
|
return Err(IbcError::InvalidPacketSequence {
|
||||||
|
expected: 1,
|
||||||
|
actual: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.source_port.validate()?;
|
||||||
|
self.dest_port.validate()?;
|
||||||
|
|
||||||
|
if self.data.len() > MAX_PACKET_DATA_SIZE {
|
||||||
|
return Err(IbcError::PacketDataTooLarge {
|
||||||
|
size: self.data.len(),
|
||||||
|
max: MAX_PACKET_DATA_SIZE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one timeout must be set
|
||||||
|
if self.timeout_height.is_zero() && self.timeout_timestamp.is_zero() {
|
||||||
|
return Err(IbcError::InvalidCommitment(
|
||||||
|
"at least one timeout must be set".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute packet commitment hash
|
||||||
|
pub fn commitment(&self) -> PacketCommitment {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&self.timeout_timestamp.nanoseconds().to_be_bytes());
|
||||||
|
hasher.update(&self.timeout_height.revision_number.to_be_bytes());
|
||||||
|
hasher.update(&self.timeout_height.revision_height.to_be_bytes());
|
||||||
|
|
||||||
|
// Hash the data
|
||||||
|
let data_hash = Sha256::digest(&self.data);
|
||||||
|
hasher.update(&data_hash);
|
||||||
|
|
||||||
|
PacketCommitment(hasher.finalize().to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet commitment hash
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct PacketCommitment(pub Vec<u8>);
|
||||||
|
|
||||||
|
impl PacketCommitment {
|
||||||
|
/// Create from bytes
|
||||||
|
pub fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get as bytes
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get as hex string
|
||||||
|
pub fn to_hex(&self) -> String {
|
||||||
|
hex::encode(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet acknowledgement
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum Acknowledgement {
|
||||||
|
/// Success result with optional data
|
||||||
|
Success(Vec<u8>),
|
||||||
|
/// Error result
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Acknowledgement {
|
||||||
|
/// Create a success acknowledgement
|
||||||
|
pub fn success(data: Vec<u8>) -> Self {
|
||||||
|
Acknowledgement::Success(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an error acknowledgement
|
||||||
|
pub fn error(msg: impl Into<String>) -> Self {
|
||||||
|
Acknowledgement::Error(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if acknowledgement is success
|
||||||
|
pub fn is_success(&self) -> bool {
|
||||||
|
matches!(self, Acknowledgement::Success(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute acknowledgement commitment
|
||||||
|
pub fn commitment(&self) -> PacketCommitment {
|
||||||
|
let bytes = match self {
|
||||||
|
Acknowledgement::Success(data) => {
|
||||||
|
let mut result = vec![0x01]; // Success prefix
|
||||||
|
result.extend(data);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
Acknowledgement::Error(msg) => {
|
||||||
|
let mut result = vec![0x00]; // Error prefix
|
||||||
|
result.extend(msg.as_bytes());
|
||||||
|
result
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash = Sha256::digest(&bytes);
|
||||||
|
PacketCommitment(hash.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode to bytes
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
serde_json::to_vec(self).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from bytes
|
||||||
|
pub fn decode(bytes: &[u8]) -> IbcResult<Self> {
|
||||||
|
serde_json::from_slice(bytes)
|
||||||
|
.map_err(|e| IbcError::InvalidAcknowledgement(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout information
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct Timeout {
|
||||||
|
/// Timeout height
|
||||||
|
pub height: Height,
|
||||||
|
/// Timeout timestamp
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timeout {
|
||||||
|
/// Create a new timeout
|
||||||
|
pub fn new(height: Height, timestamp: Timestamp) -> Self {
|
||||||
|
Self { height, timestamp }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a height-only timeout
|
||||||
|
pub fn height(height: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Height::from_height(height),
|
||||||
|
timestamp: Timestamp::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a timestamp-only timeout
|
||||||
|
pub fn timestamp(timestamp: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Height::default(),
|
||||||
|
timestamp: Timestamp::from_nanoseconds(timestamp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if timed out
|
||||||
|
pub fn is_timed_out(&self, current_height: Height, current_time: Timestamp) -> bool {
|
||||||
|
(!self.height.is_zero() && current_height >= self.height)
|
||||||
|
|| (!self.timestamp.is_zero() && current_time >= self.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timeout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
height: Height::default(),
|
||||||
|
timestamp: Timestamp::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet receipt (for unordered channels)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PacketReceipt {
|
||||||
|
/// Received at height
|
||||||
|
pub received_at: Height,
|
||||||
|
/// Acknowledgement (if processed)
|
||||||
|
pub acknowledgement: Option<Acknowledgement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet state tracking
|
||||||
|
pub struct PacketHandler {
|
||||||
|
/// Sent packet commitments by (port, channel, sequence)
|
||||||
|
sent_commitments: HashMap<(PortId, ChannelId, u64), PacketCommitment>,
|
||||||
|
/// Received packet receipts by (port, channel, sequence)
|
||||||
|
receipts: HashMap<(PortId, ChannelId, u64), PacketReceipt>,
|
||||||
|
/// Acknowledgement commitments by (port, channel, sequence)
|
||||||
|
ack_commitments: HashMap<(PortId, ChannelId, u64), PacketCommitment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketHandler {
|
||||||
|
/// Create a new packet handler
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sent_commitments: HashMap::new(),
|
||||||
|
receipts: HashMap::new(),
|
||||||
|
ack_commitments: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a sent packet commitment
|
||||||
|
pub fn store_packet_commitment(&mut self, packet: &Packet) {
|
||||||
|
let key = (
|
||||||
|
packet.source_port.clone(),
|
||||||
|
packet.source_channel.clone(),
|
||||||
|
packet.sequence,
|
||||||
|
);
|
||||||
|
self.sent_commitments.insert(key, packet.commitment());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get packet commitment
|
||||||
|
pub fn get_packet_commitment(
|
||||||
|
&self,
|
||||||
|
port: &PortId,
|
||||||
|
channel: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> Option<&PacketCommitment> {
|
||||||
|
self.sent_commitments
|
||||||
|
.get(&(port.clone(), channel.clone(), sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete packet commitment (after ack received)
|
||||||
|
pub fn delete_packet_commitment(&mut self, port: &PortId, channel: &ChannelId, sequence: u64) {
|
||||||
|
self.sent_commitments
|
||||||
|
.remove(&(port.clone(), channel.clone(), sequence));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store packet receipt
|
||||||
|
pub fn store_packet_receipt(
|
||||||
|
&mut self,
|
||||||
|
packet: &Packet,
|
||||||
|
current_height: Height,
|
||||||
|
ack: Option<Acknowledgement>,
|
||||||
|
) {
|
||||||
|
let key = (
|
||||||
|
packet.dest_port.clone(),
|
||||||
|
packet.dest_channel.clone(),
|
||||||
|
packet.sequence,
|
||||||
|
);
|
||||||
|
self.receipts.insert(
|
||||||
|
key,
|
||||||
|
PacketReceipt {
|
||||||
|
received_at: current_height,
|
||||||
|
acknowledgement: ack,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if packet was received
|
||||||
|
pub fn has_packet_receipt(
|
||||||
|
&self,
|
||||||
|
port: &PortId,
|
||||||
|
channel: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> bool {
|
||||||
|
self.receipts
|
||||||
|
.contains_key(&(port.clone(), channel.clone(), sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store acknowledgement commitment
|
||||||
|
pub fn store_ack_commitment(
|
||||||
|
&mut self,
|
||||||
|
port: &PortId,
|
||||||
|
channel: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
ack: &Acknowledgement,
|
||||||
|
) {
|
||||||
|
let key = (port.clone(), channel.clone(), sequence);
|
||||||
|
self.ack_commitments.insert(key, ack.commitment());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get acknowledgement commitment
|
||||||
|
pub fn get_ack_commitment(
|
||||||
|
&self,
|
||||||
|
port: &PortId,
|
||||||
|
channel: &ChannelId,
|
||||||
|
sequence: u64,
|
||||||
|
) -> Option<&PacketCommitment> {
|
||||||
|
self.ack_commitments
|
||||||
|
.get(&(port.clone(), channel.clone(), sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete acknowledgement commitment (after processed)
|
||||||
|
pub fn delete_ack_commitment(&mut self, port: &PortId, channel: &ChannelId, sequence: u64) {
|
||||||
|
self.ack_commitments
|
||||||
|
.remove(&(port.clone(), channel.clone(), sequence));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify packet hasn't already been received (for unordered channels)
|
||||||
|
pub fn verify_no_receipt(&self, packet: &Packet) -> IbcResult<()> {
|
||||||
|
if self.has_packet_receipt(&packet.dest_port, &packet.dest_channel, packet.sequence) {
|
||||||
|
return Err(IbcError::PacketAlreadyReceived(packet.sequence));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify packet sequence for ordered channels
|
||||||
|
pub fn verify_ordered_sequence(
|
||||||
|
&self,
|
||||||
|
packet: &Packet,
|
||||||
|
expected_sequence: u64,
|
||||||
|
) -> IbcResult<()> {
|
||||||
|
if packet.sequence != expected_sequence {
|
||||||
|
return Err(IbcError::InvalidPacketSequence {
|
||||||
|
expected: expected_sequence,
|
||||||
|
actual: packet.sequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process timeout for a packet
|
||||||
|
pub fn timeout_packet(&mut self, packet: &Packet) -> IbcResult<()> {
|
||||||
|
// Verify commitment exists
|
||||||
|
let key = (
|
||||||
|
packet.source_port.clone(),
|
||||||
|
packet.source_channel.clone(),
|
||||||
|
packet.sequence,
|
||||||
|
);
|
||||||
|
if !self.sent_commitments.contains_key(&key) {
|
||||||
|
return Err(IbcError::PacketNotFound(packet.sequence));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove commitment
|
||||||
|
self.sent_commitments.remove(&key);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PacketHandler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfer packet data (ICS-20)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FungibleTokenPacketData {
|
||||||
|
/// Token denomination
|
||||||
|
pub denom: String,
|
||||||
|
/// Amount to transfer
|
||||||
|
pub amount: String,
|
||||||
|
/// Sender address
|
||||||
|
pub sender: String,
|
||||||
|
/// Receiver address
|
||||||
|
pub receiver: String,
|
||||||
|
/// Optional memo
|
||||||
|
pub memo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FungibleTokenPacketData {
|
||||||
|
/// Create a new transfer packet
|
||||||
|
pub fn new(
|
||||||
|
denom: impl Into<String>,
|
||||||
|
amount: impl Into<String>,
|
||||||
|
sender: impl Into<String>,
|
||||||
|
receiver: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
denom: denom.into(),
|
||||||
|
amount: amount.into(),
|
||||||
|
sender: sender.into(),
|
||||||
|
receiver: receiver.into(),
|
||||||
|
memo: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add memo
|
||||||
|
pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
|
||||||
|
self.memo = memo.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode to bytes
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
serde_json::to_vec(self).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from bytes
|
||||||
|
pub fn decode(bytes: &[u8]) -> IbcResult<Self> {
|
||||||
|
serde_json::from_slice(bytes)
|
||||||
|
.map_err(|e| IbcError::DeserializationError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the denomination trace path
|
||||||
|
pub fn get_denom_trace(&self) -> Vec<String> {
|
||||||
|
self.denom.split('/').map(String::from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this is a native token
|
||||||
|
pub fn is_native(&self) -> bool {
|
||||||
|
!self.denom.contains('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_packet() -> Packet {
|
||||||
|
Packet::new(
|
||||||
|
1,
|
||||||
|
PortId::transfer(),
|
||||||
|
ChannelId::new(0),
|
||||||
|
PortId::transfer(),
|
||||||
|
ChannelId::new(0),
|
||||||
|
b"test data".to_vec(),
|
||||||
|
Height::from_height(100),
|
||||||
|
Timestamp::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_commitment() {
|
||||||
|
let packet = test_packet();
|
||||||
|
let commitment = packet.commitment();
|
||||||
|
assert!(!commitment.as_bytes().is_empty());
|
||||||
|
|
||||||
|
// Same packet should have same commitment
|
||||||
|
let commitment2 = packet.commitment();
|
||||||
|
assert_eq!(commitment, commitment2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_timeout() {
|
||||||
|
let packet = test_packet();
|
||||||
|
|
||||||
|
// Not timed out at lower height
|
||||||
|
assert!(!packet.is_timed_out_by_height(Height::from_height(50)));
|
||||||
|
|
||||||
|
// Timed out at same height
|
||||||
|
assert!(packet.is_timed_out_by_height(Height::from_height(100)));
|
||||||
|
|
||||||
|
// Timed out at higher height
|
||||||
|
assert!(packet.is_timed_out_by_height(Height::from_height(150)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_acknowledgement() {
|
||||||
|
let ack = Acknowledgement::success(b"ok".to_vec());
|
||||||
|
assert!(ack.is_success());
|
||||||
|
|
||||||
|
let ack_err = Acknowledgement::error("failed");
|
||||||
|
assert!(!ack_err.is_success());
|
||||||
|
|
||||||
|
// Commitments should be different
|
||||||
|
assert_ne!(ack.commitment(), ack_err.commitment());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_handler() {
|
||||||
|
let mut handler = PacketHandler::new();
|
||||||
|
let packet = test_packet();
|
||||||
|
|
||||||
|
// Store commitment
|
||||||
|
handler.store_packet_commitment(&packet);
|
||||||
|
assert!(handler
|
||||||
|
.get_packet_commitment(&packet.source_port, &packet.source_channel, packet.sequence)
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
// Store receipt
|
||||||
|
handler.store_packet_receipt(&packet, Height::from_height(50), None);
|
||||||
|
assert!(handler.has_packet_receipt(
|
||||||
|
&packet.dest_port,
|
||||||
|
&packet.dest_channel,
|
||||||
|
packet.sequence
|
||||||
|
));
|
||||||
|
|
||||||
|
// Verify no duplicate receipt
|
||||||
|
assert!(handler.verify_no_receipt(&packet).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transfer_packet_data() {
|
||||||
|
let data = FungibleTokenPacketData::new("uatom", "1000000", "cosmos1...", "synor1...");
|
||||||
|
|
||||||
|
let encoded = data.encode();
|
||||||
|
let decoded = FungibleTokenPacketData::decode(&encoded).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoded.denom, data.denom);
|
||||||
|
assert_eq!(decoded.amount, data.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_denom_trace() {
|
||||||
|
// Native token
|
||||||
|
let native = FungibleTokenPacketData::new("usynor", "1000", "a", "b");
|
||||||
|
assert!(native.is_native());
|
||||||
|
|
||||||
|
// IBC token
|
||||||
|
let ibc = FungibleTokenPacketData::new("transfer/channel-0/uatom", "1000", "a", "b");
|
||||||
|
assert!(!ibc.is_native());
|
||||||
|
assert_eq!(ibc.get_denom_trace(), vec!["transfer", "channel-0", "uatom"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
1020
crates/synor-ibc/src/swap.rs
Normal file
1020
crates/synor-ibc/src/swap.rs
Normal file
File diff suppressed because it is too large
Load diff
295
crates/synor-ibc/src/types.rs
Normal file
295
crates/synor-ibc/src/types.rs
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
//! Common IBC types and identifiers
|
||||||
|
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Height representation for IBC
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Height {
|
||||||
|
/// Revision number (for hard forks)
|
||||||
|
pub revision_number: u64,
|
||||||
|
/// Block height
|
||||||
|
pub revision_height: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Height {
|
||||||
|
/// Create a new height
|
||||||
|
pub fn new(revision_number: u64, revision_height: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
revision_number,
|
||||||
|
revision_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create height with revision 0
|
||||||
|
pub fn from_height(height: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
revision_number: 0,
|
||||||
|
revision_height: height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this height is zero
|
||||||
|
pub fn is_zero(&self) -> bool {
|
||||||
|
self.revision_number == 0 && self.revision_height == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the height
|
||||||
|
pub fn increment(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
revision_number: self.revision_number,
|
||||||
|
revision_height: self.revision_height + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrement the height
|
||||||
|
pub fn decrement(&self) -> Option<Self> {
|
||||||
|
if self.revision_height == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Self {
|
||||||
|
revision_number: self.revision_number,
|
||||||
|
revision_height: self.revision_height - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Height {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
revision_number: 0,
|
||||||
|
revision_height: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Height {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}-{}", self.revision_number, self.revision_height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timestamp in nanoseconds since Unix epoch
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||||
|
Serialize, Deserialize, BorshSerialize, BorshDeserialize,
|
||||||
|
)]
|
||||||
|
pub struct Timestamp(pub u64);
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
/// Create from nanoseconds
|
||||||
|
pub fn from_nanoseconds(nanos: u64) -> Self {
|
||||||
|
Self(nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from seconds
|
||||||
|
pub fn from_seconds(secs: u64) -> Self {
|
||||||
|
Self(secs * 1_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get nanoseconds
|
||||||
|
pub fn nanoseconds(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get seconds
|
||||||
|
pub fn seconds(&self) -> u64 {
|
||||||
|
self.0 / 1_000_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if zero (disabled timeout)
|
||||||
|
pub fn is_zero(&self) -> bool {
|
||||||
|
self.0 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp
|
||||||
|
pub fn now() -> Self {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let duration = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards");
|
||||||
|
Self(duration.as_nanos() as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timestamp {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Timestamp {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IBC version with features
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Version {
|
||||||
|
/// Version identifier
|
||||||
|
pub identifier: String,
|
||||||
|
/// Supported features
|
||||||
|
pub features: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
/// Create a new version
|
||||||
|
pub fn new(identifier: impl Into<String>, features: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
identifier: identifier.into(),
|
||||||
|
features,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default IBC version for connections
|
||||||
|
pub fn default_connection() -> Self {
|
||||||
|
Self {
|
||||||
|
identifier: "1".to_string(),
|
||||||
|
features: vec!["ORDER_ORDERED".to_string(), "ORDER_UNORDERED".to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a feature is supported
|
||||||
|
pub fn has_feature(&self, feature: &str) -> bool {
|
||||||
|
self.features.iter().any(|f| f == feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Version {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::default_connection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefix for commitment paths
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CommitmentPrefix {
|
||||||
|
/// Key prefix bytes
|
||||||
|
pub key_prefix: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommitmentPrefix {
|
||||||
|
/// Create a new prefix
|
||||||
|
pub fn new(prefix: impl Into<Vec<u8>>) -> Self {
|
||||||
|
Self {
|
||||||
|
key_prefix: prefix.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get as string (if valid UTF-8)
|
||||||
|
pub fn as_str(&self) -> Option<&str> {
|
||||||
|
std::str::from_utf8(&self.key_prefix).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CommitmentPrefix {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
key_prefix: b"ibc".to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signer address
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Signer(pub String);
|
||||||
|
|
||||||
|
impl Signer {
|
||||||
|
/// Create a new signer
|
||||||
|
pub fn new(address: impl Into<String>) -> Self {
|
||||||
|
Self(address.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get address string
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Signer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chain identifier
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct ChainId(pub String);
|
||||||
|
|
||||||
|
impl ChainId {
|
||||||
|
/// Create a new chain ID
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
Self(id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synor mainnet chain ID
|
||||||
|
pub fn synor_mainnet() -> Self {
|
||||||
|
Self("synor-1".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synor testnet chain ID
|
||||||
|
pub fn synor_testnet() -> Self {
|
||||||
|
Self("synor-testnet-1".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get revision number from chain ID
|
||||||
|
pub fn revision_number(&self) -> u64 {
|
||||||
|
// Parse revision from format "chain-name-N"
|
||||||
|
self.0
|
||||||
|
.rsplit('-')
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ChainId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_height() {
|
||||||
|
let h = Height::new(1, 100);
|
||||||
|
assert_eq!(h.revision_number, 1);
|
||||||
|
assert_eq!(h.revision_height, 100);
|
||||||
|
|
||||||
|
let h2 = h.increment();
|
||||||
|
assert_eq!(h2.revision_height, 101);
|
||||||
|
|
||||||
|
let h3 = h2.decrement().unwrap();
|
||||||
|
assert_eq!(h3.revision_height, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp() {
|
||||||
|
let ts = Timestamp::from_seconds(1000);
|
||||||
|
assert_eq!(ts.nanoseconds(), 1_000_000_000_000);
|
||||||
|
assert_eq!(ts.seconds(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version() {
|
||||||
|
let v = Version::default_connection();
|
||||||
|
assert!(v.has_feature("ORDER_ORDERED"));
|
||||||
|
assert!(v.has_feature("ORDER_UNORDERED"));
|
||||||
|
assert!(!v.has_feature("UNKNOWN"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chain_id_revision() {
|
||||||
|
let chain = ChainId::new("cosmos-hub-4");
|
||||||
|
assert_eq!(chain.revision_number(), 4);
|
||||||
|
|
||||||
|
let chain = ChainId::new("synor-testnet-1");
|
||||||
|
assert_eq!(chain.revision_number(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue