use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::hash::Hash;
use crate::right::{OwnershipProof, Right};
use crate::seal::SealRef;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum ChainId {
Bitcoin,
Sui,
Aptos,
Ethereum,
}
impl ChainId {
pub fn as_u8(&self) -> u8 {
match self {
ChainId::Bitcoin => 0,
ChainId::Sui => 1,
ChainId::Aptos => 2,
ChainId::Ethereum => 3,
}
}
pub fn from_u8(id: u8) -> Option<Self> {
match id {
0 => Some(ChainId::Bitcoin),
1 => Some(ChainId::Sui),
2 => Some(ChainId::Aptos),
3 => Some(ChainId::Ethereum),
_ => None,
}
}
}
impl core::fmt::Display for ChainId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ChainId::Bitcoin => write!(f, "Bitcoin"),
ChainId::Sui => write!(f, "Sui"),
ChainId::Aptos => write!(f, "Aptos"),
ChainId::Ethereum => write!(f, "Ethereum"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossChainLockEvent {
pub right_id: Hash,
pub commitment: Hash,
pub owner: OwnershipProof,
pub source_chain: ChainId,
pub destination_chain: ChainId,
pub destination_owner: OwnershipProof,
pub source_seal: SealRef,
pub source_tx_hash: Hash,
pub source_block_height: u64,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum InclusionProof {
Bitcoin(BitcoinMerkleProof),
Ethereum(EthereumMPTProof),
Sui(SuiCheckpointProof),
Aptos(AptosLedgerProof),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct BitcoinMerkleProof {
pub txid: [u8; 32],
pub merkle_branch: Vec<[u8; 32]>,
pub block_header: Vec<u8>,
pub block_height: u64,
pub confirmations: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct EthereumMPTProof {
pub tx_hash: [u8; 32],
pub receipt_root: [u8; 32],
pub receipt_rlp: Vec<u8>,
pub merkle_nodes: Vec<Vec<u8>>,
pub block_header: Vec<u8>,
pub log_index: u64,
pub confirmations: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct SuiCheckpointProof {
pub tx_digest: [u8; 32],
pub checkpoint_sequence: u64,
pub checkpoint_contents_hash: [u8; 32],
pub effects: Vec<u8>,
pub events: Vec<u8>,
pub certified: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct AptosLedgerProof {
pub version: u64,
pub transaction_proof: Vec<u8>,
pub ledger_info: Vec<u8>,
pub events: Vec<u8>,
pub success: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossChainFinalityProof {
pub source_chain: ChainId,
pub height: u64,
pub current_height: u64,
pub is_finalized: bool,
pub depth: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossChainTransferProof {
pub lock_event: CrossChainLockEvent,
pub inclusion_proof: InclusionProof,
pub finality_proof: CrossChainFinalityProof,
pub source_state_root: Hash,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossChainRegistryEntry {
pub right_id: Hash,
pub source_chain: ChainId,
pub source_seal: SealRef,
pub destination_chain: ChainId,
pub destination_seal: SealRef,
pub lock_tx_hash: Hash,
pub mint_tx_hash: Hash,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CrossChainTransferResult {
pub destination_right: Right,
pub destination_seal: SealRef,
pub registry_entry: CrossChainRegistryEntry,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[allow(missing_docs)]
pub enum CrossChainError {
#[error("Right already locked on source chain")]
AlreadyLocked,
#[error("Right already exists on destination chain")]
AlreadyMinted,
#[error("Invalid inclusion proof")]
InvalidInclusionProof,
#[error("Insufficient finality: {0} confirmations, need {1}")]
InsufficientFinality(u64, u64),
#[error("Ownership proof verification failed")]
InvalidOwnership,
#[error("Lock event does not match expected data")]
LockEventMismatch,
#[error("Cross-chain registry error: {0}")]
RegistryError(String),
#[error("Unsupported chain pair: {0} → {1}")]
UnsupportedChainPair(ChainId, ChainId),
}
pub trait LockProvider {
fn lock_right(
&self,
right_id: Hash,
commitment: Hash,
owner: OwnershipProof,
destination_chain: ChainId,
destination_owner: OwnershipProof,
) -> Result<(CrossChainLockEvent, InclusionProof), CrossChainError>;
}
pub trait TransferVerifier {
fn verify_transfer_proof(&self, proof: &CrossChainTransferProof)
-> Result<(), CrossChainError>;
}
pub trait MintProvider {
fn mint_right(
&self,
proof: &CrossChainTransferProof,
) -> Result<CrossChainTransferResult, CrossChainError>;
}
pub struct CrossChainTransfer {
pub registry: CrossChainRegistry,
}
impl CrossChainTransfer {
pub fn new(registry: CrossChainRegistry) -> Self {
Self { registry }
}
pub fn execute(
&mut self,
locker: &dyn LockProvider,
verifier: &dyn TransferVerifier,
minter: &dyn MintProvider,
right_id: Hash,
commitment: Hash,
owner: OwnershipProof,
destination_chain: ChainId,
destination_owner: OwnershipProof,
current_block_height: u64,
finality_depth: u64,
) -> Result<CrossChainTransferResult, CrossChainError> {
let (lock_event, inclusion_proof) = locker.lock_right(
right_id,
commitment,
owner.clone(),
destination_chain.clone(),
destination_owner.clone(),
)?;
let source_chain = lock_event.source_chain.clone();
let source_block_height = lock_event.source_block_height;
let lock_timestamp = lock_event.timestamp;
let is_finalized = current_block_height >= source_block_height + finality_depth;
let transfer_proof = CrossChainTransferProof {
lock_event,
inclusion_proof,
finality_proof: CrossChainFinalityProof {
source_chain: source_chain.clone(),
height: source_block_height,
current_height: current_block_height,
is_finalized,
depth: finality_depth,
},
source_state_root: Hash::new([0u8; 32]),
};
verifier.verify_transfer_proof(&transfer_proof)?;
let result = minter.mint_right(&transfer_proof)?;
let entry = CrossChainRegistryEntry {
right_id,
source_chain,
source_seal: transfer_proof.lock_event.source_seal.clone(),
destination_chain: transfer_proof.lock_event.destination_chain.clone(),
destination_seal: result.destination_seal.clone(),
lock_tx_hash: transfer_proof.lock_event.source_tx_hash,
mint_tx_hash: Hash::new([0u8; 32]),
timestamp: lock_timestamp,
};
self.registry.record_transfer(entry)?;
Ok(result)
}
}
#[derive(Default)]
pub struct CrossChainRegistry {
entries: alloc::collections::BTreeMap<Hash, CrossChainRegistryEntry>,
}
impl CrossChainRegistry {
pub fn new() -> Self {
Self {
entries: alloc::collections::BTreeMap::new(),
}
}
pub fn record_transfer(
&mut self,
entry: CrossChainRegistryEntry,
) -> Result<(), CrossChainError> {
if self.entries.contains_key(&entry.right_id) {
return Err(CrossChainError::AlreadyMinted);
}
for existing in self.entries.values() {
if existing.source_seal == entry.source_seal {
return Err(CrossChainError::AlreadyLocked);
}
}
self.entries.insert(entry.right_id, entry);
Ok(())
}
pub fn is_right_transferred(&self, right_id: &Hash) -> bool {
self.entries.contains_key(right_id)
}
pub fn is_seal_consumed(&self, seal: &SealRef) -> bool {
self.entries.values().any(|e| &e.source_seal == seal)
}
pub fn get_entry(&self, right_id: &Hash) -> Option<&CrossChainRegistryEntry> {
self.entries.get(right_id)
}
pub fn transfer_count(&self) -> usize {
self.entries.len()
}
pub fn all_transfers(&self) -> Vec<&CrossChainRegistryEntry> {
self.entries.values().collect()
}
}
pub use crate::seal_registry::SealConsumption;
pub use crate::seal_registry::CrossChainSealRegistry;
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Hash;
#[test]
fn test_chain_id_roundtrip() {
for chain in [
ChainId::Bitcoin,
ChainId::Sui,
ChainId::Aptos,
ChainId::Ethereum,
] {
let id = chain.as_u8();
assert_eq!(ChainId::from_u8(id), Some(chain));
}
assert_eq!(ChainId::from_u8(99), None);
}
#[test]
fn test_registry_prevents_double_mint() {
let mut registry = CrossChainRegistry::new();
let right_id = Hash::new([0xAB; 32]);
let entry = CrossChainRegistryEntry {
right_id,
source_chain: ChainId::Bitcoin,
source_seal: SealRef::new(vec![0x01], None).unwrap(),
destination_chain: ChainId::Sui,
destination_seal: SealRef::new(vec![0x02], None).unwrap(),
lock_tx_hash: Hash::new([0x03; 32]),
mint_tx_hash: Hash::new([0x04; 32]),
timestamp: 1_000_000,
};
registry.record_transfer(entry.clone()).unwrap();
let result = registry.record_transfer(entry);
assert!(matches!(result, Err(CrossChainError::AlreadyMinted)));
}
#[test]
fn test_registry_prevents_double_lock() {
let mut registry = CrossChainRegistry::new();
let seal = SealRef::new(vec![0x01], None).unwrap();
let entry1 = CrossChainRegistryEntry {
right_id: Hash::new([0xAB; 32]),
source_chain: ChainId::Bitcoin,
source_seal: seal.clone(),
destination_chain: ChainId::Sui,
destination_seal: SealRef::new(vec![0x02], None).unwrap(),
lock_tx_hash: Hash::new([0x03; 32]),
mint_tx_hash: Hash::new([0x04; 32]),
timestamp: 1_000_000,
};
registry.record_transfer(entry1).unwrap();
let entry2 = CrossChainRegistryEntry {
right_id: Hash::new([0xCD; 32]),
source_chain: ChainId::Bitcoin,
source_seal: seal.clone(),
destination_chain: ChainId::Aptos,
destination_seal: SealRef::new(vec![0x05], None).unwrap(),
lock_tx_hash: Hash::new([0x06; 32]),
mint_tx_hash: Hash::new([0x07; 32]),
timestamp: 2_000_000,
};
let result = registry.record_transfer(entry2);
assert!(matches!(result, Err(CrossChainError::AlreadyLocked)));
}
#[test]
fn test_registry_tracks_transfers() {
let mut registry = CrossChainRegistry::new();
assert_eq!(registry.transfer_count(), 0);
let entry = CrossChainRegistryEntry {
right_id: Hash::new([0xAB; 32]),
source_chain: ChainId::Bitcoin,
source_seal: SealRef::new(vec![0x01], None).unwrap(),
destination_chain: ChainId::Sui,
destination_seal: SealRef::new(vec![0x02], None).unwrap(),
lock_tx_hash: Hash::new([0x03; 32]),
mint_tx_hash: Hash::new([0x04; 32]),
timestamp: 1_000_000,
};
registry.record_transfer(entry).unwrap();
assert_eq!(registry.transfer_count(), 1);
assert!(registry.is_right_transferred(&Hash::new([0xAB; 32])));
}
}