Skip to main content

csv_adapter_core/
cross_chain.rs

1//! Cross-Chain Right Transfer
2//!
3//! Implements the lock-and-prove protocol for transferring Rights between chains:
4//! 1. Lock — Source chain consumes seal, emits CrossChainLockEvent
5//! 2. Prove — Client generates inclusion proof
6//! 3. Verify — Destination chain verifies proof, checks registry, mints new Right
7//! 4. Registry — Records transfer, prevents cross-chain double-spend
8
9use alloc::vec::Vec;
10use serde::{Deserialize, Serialize};
11
12use crate::hash::Hash;
13use crate::right::{OwnershipProof, Right};
14use crate::seal::SealRef;
15
16/// Chain identifier for cross-chain transfers.
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[allow(missing_docs)]
19pub enum ChainId {
20    /// Bitcoin (UTXO seals)
21    Bitcoin,
22    /// Sui (Object seals)
23    Sui,
24    /// Aptos (Resource seals)
25    Aptos,
26    /// Ethereum (Nullifier seals)
27    Ethereum,
28}
29
30impl ChainId {
31    /// Get a numeric identifier for serialization.
32    pub fn as_u8(&self) -> u8 {
33        match self {
34            ChainId::Bitcoin => 0,
35            ChainId::Sui => 1,
36            ChainId::Aptos => 2,
37            ChainId::Ethereum => 3,
38        }
39    }
40
41    /// Parse a chain ID from a u8.
42    pub fn from_u8(id: u8) -> Option<Self> {
43        match id {
44            0 => Some(ChainId::Bitcoin),
45            1 => Some(ChainId::Sui),
46            2 => Some(ChainId::Aptos),
47            3 => Some(ChainId::Ethereum),
48            _ => None,
49        }
50    }
51}
52
53impl core::fmt::Display for ChainId {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        match self {
56            ChainId::Bitcoin => write!(f, "Bitcoin"),
57            ChainId::Sui => write!(f, "Sui"),
58            ChainId::Aptos => write!(f, "Aptos"),
59            ChainId::Ethereum => write!(f, "Ethereum"),
60        }
61    }
62}
63
64/// Event emitted when a Right is locked on the source chain for cross-chain transfer.
65#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct CrossChainLockEvent {
67    /// The Right being locked
68    pub right_id: Hash,
69    /// The commitment hash of the Right
70    pub commitment: Hash,
71    /// The owner who initiated the lock
72    pub owner: OwnershipProof,
73    /// Source chain where the Right is being locked
74    pub source_chain: ChainId,
75    /// Destination chain for the transfer
76    pub destination_chain: ChainId,
77    /// Destination owner (may differ from source owner)
78    pub destination_owner: OwnershipProof,
79    /// Source chain's seal reference (consumed during lock)
80    pub source_seal: SealRef,
81    /// Source transaction hash
82    pub source_tx_hash: Hash,
83    /// Source block height
84    pub source_block_height: u64,
85    /// Unix timestamp of the lock event
86    pub timestamp: u64,
87}
88
89/// Inclusion proof — chain-specific format.
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub enum InclusionProof {
92    /// Bitcoin: Merkle branch + block header
93    Bitcoin(BitcoinMerkleProof),
94    /// Ethereum: MPT receipt proof
95    Ethereum(EthereumMPTProof),
96    /// Sui: Checkpoint certification
97    Sui(SuiCheckpointProof),
98    /// Aptos: Ledger info proof
99    Aptos(AptosLedgerProof),
100}
101
102/// Bitcoin Merkle proof of transaction inclusion in a block.
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104#[allow(missing_docs)]
105pub struct BitcoinMerkleProof {
106    /// Transaction ID
107    pub txid: [u8; 32],
108    /// Merkle branch nodes
109    pub merkle_branch: Vec<[u8; 32]>,
110    /// Serialized block header
111    pub block_header: Vec<u8>,
112    /// Block height
113    pub block_height: u64,
114    /// Number of confirmations
115    pub confirmations: u64,
116}
117
118/// Ethereum MPT proof of receipt inclusion in state.
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120#[allow(missing_docs)]
121pub struct EthereumMPTProof {
122    /// Transaction hash
123    pub tx_hash: [u8; 32],
124    /// Receipt root hash
125    pub receipt_root: [u8; 32],
126    /// RLP-encoded receipt
127    pub receipt_rlp: Vec<u8>,
128    /// MPT proof nodes
129    pub merkle_nodes: Vec<Vec<u8>>,
130    /// Serialized block header
131    pub block_header: Vec<u8>,
132    /// Log index in the receipt
133    pub log_index: u64,
134    /// Number of confirmations
135    pub confirmations: u64,
136}
137
138/// Sui checkpoint proof of transaction effects certification.
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140#[allow(missing_docs)]
141pub struct SuiCheckpointProof {
142    /// Transaction digest
143    pub tx_digest: [u8; 32],
144    /// Checkpoint sequence number
145    pub checkpoint_sequence: u64,
146    /// Checkpoint contents hash
147    pub checkpoint_contents_hash: [u8; 32],
148    /// Transaction effects bytes
149    pub effects: Vec<u8>,
150    /// Event bytes
151    pub events: Vec<u8>,
152    /// Whether the checkpoint is certified
153    pub certified: bool,
154}
155
156/// Aptos ledger info proof of transaction execution.
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158#[allow(missing_docs)]
159pub struct AptosLedgerProof {
160    /// Transaction version
161    pub version: u64,
162    /// Transaction proof bytes
163    pub transaction_proof: Vec<u8>,
164    /// Ledger info bytes
165    pub ledger_info: Vec<u8>,
166    /// Event bytes
167    pub events: Vec<u8>,
168    /// Whether the transaction succeeded
169    pub success: bool,
170}
171
172/// Finality proof confirming source transaction is finalized.
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
174pub struct CrossChainFinalityProof {
175    /// Source chain identifier
176    pub source_chain: ChainId,
177    /// Block/checkpoint/ledger height of the transaction
178    pub height: u64,
179    /// Current height on the source chain
180    pub current_height: u64,
181    /// Whether finality depth has been achieved
182    pub is_finalized: bool,
183    /// Required finality depth in blocks
184    pub depth: u64,
185}
186
187/// Complete proof bundle submitted to the destination chain.
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189pub struct CrossChainTransferProof {
190    /// The lock event data from the source chain
191    pub lock_event: CrossChainLockEvent,
192    /// Inclusion proof (chain-specific format)
193    pub inclusion_proof: InclusionProof,
194    /// Finality proof confirming source transaction
195    pub finality_proof: CrossChainFinalityProof,
196    /// Source chain's state root at the lock block
197    pub source_state_root: Hash,
198}
199
200/// Entry in the cross-chain seal registry recording a completed transfer.
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct CrossChainRegistryEntry {
203    /// The Right's unique ID (preserved across chains)
204    pub right_id: Hash,
205    /// Source chain identifier
206    pub source_chain: ChainId,
207    /// Source chain's seal reference
208    pub source_seal: SealRef,
209    /// Destination chain identifier
210    pub destination_chain: ChainId,
211    /// Destination chain's seal reference
212    pub destination_seal: SealRef,
213    /// Lock transaction hash on source chain
214    pub lock_tx_hash: Hash,
215    /// Mint transaction hash on destination chain
216    pub mint_tx_hash: Hash,
217    /// Unix timestamp of the transfer
218    pub timestamp: u64,
219}
220
221/// Result of a successful cross-chain transfer.
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct CrossChainTransferResult {
224    /// The new Right created on the destination chain
225    pub destination_right: Right,
226    /// The destination chain's seal reference
227    pub destination_seal: SealRef,
228    /// Registry entry recording the transfer
229    pub registry_entry: CrossChainRegistryEntry,
230}
231
232/// Errors that can occur during cross-chain transfer.
233#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
234#[allow(missing_docs)]
235pub enum CrossChainError {
236    #[error("Right already locked on source chain")]
237    AlreadyLocked,
238    #[error("Right already exists on destination chain")]
239    AlreadyMinted,
240    #[error("Invalid inclusion proof")]
241    InvalidInclusionProof,
242    #[error("Insufficient finality: {0} confirmations, need {1}")]
243    InsufficientFinality(u64, u64),
244    #[error("Ownership proof verification failed")]
245    InvalidOwnership,
246    #[error("Lock event does not match expected data")]
247    LockEventMismatch,
248    #[error("Cross-chain registry error: {0}")]
249    RegistryError(String),
250    #[error("Unsupported chain pair: {0} → {1}")]
251    UnsupportedChainPair(ChainId, ChainId),
252}
253
254/// Trait for locking a Right on a source chain.
255///
256/// Consumes the Right's seal and returns the lock event data + inclusion proof.
257pub trait LockProvider {
258    /// Lock a Right for cross-chain transfer.
259    ///
260    /// # Arguments
261    /// * `right_id` — The unique identifier of the Right
262    /// * `commitment` — The Right's commitment hash
263    /// * `owner` — Current owner's ownership proof
264    /// * `destination_chain` — Target chain ID
265    /// * `destination_owner` — New owner on destination chain
266    ///
267    /// # Returns
268    /// Lock event data and inclusion proof (chain-specific format)
269    fn lock_right(
270        &self,
271        right_id: Hash,
272        commitment: Hash,
273        owner: OwnershipProof,
274        destination_chain: ChainId,
275        destination_owner: OwnershipProof,
276    ) -> Result<(CrossChainLockEvent, InclusionProof), CrossChainError>;
277}
278
279/// Trait for verifying cross-chain transfer proofs.
280pub trait TransferVerifier {
281    /// Verify a cross-chain transfer proof.
282    ///
283    /// # Checks
284    /// 1. Inclusion proof is valid (source chain finalized)
285    /// 2. Seal NOT in CrossChainSealRegistry (no double-spend)
286    /// 3. Ownership proof valid (owner signature matches)
287    /// 4. Lock event matches expected right_id and commitment
288    fn verify_transfer_proof(&self, proof: &CrossChainTransferProof)
289        -> Result<(), CrossChainError>;
290}
291
292/// Trait for minting a Right on a destination chain.
293pub trait MintProvider {
294    /// Mint a new Right from a verified cross-chain transfer proof.
295    ///
296    /// Creates a new Right with the same commitment and state
297    /// but a new seal on the destination chain.
298    fn mint_right(
299        &self,
300        proof: &CrossChainTransferProof,
301    ) -> Result<CrossChainTransferResult, CrossChainError>;
302}
303
304/// Cross-chain transfer orchestrator.
305///
306/// Coordinates lock → prove → verify → mint across chains.
307pub struct CrossChainTransfer {
308    /// The cross-chain seal registry
309    pub registry: CrossChainRegistry,
310}
311
312impl CrossChainTransfer {
313    /// Create a new cross-chain transfer orchestrator.
314    pub fn new(registry: CrossChainRegistry) -> Self {
315        Self { registry }
316    }
317
318    /// Execute a full cross-chain transfer.
319    ///
320    /// 1. Lock the Right on the source chain
321    /// 2. Build the transfer proof
322    /// 3. Verify on the destination chain
323    /// 4. Mint the new Right
324    /// 5. Record in the registry
325    pub fn execute(
326        &mut self,
327        locker: &dyn LockProvider,
328        verifier: &dyn TransferVerifier,
329        minter: &dyn MintProvider,
330        right_id: Hash,
331        commitment: Hash,
332        owner: OwnershipProof,
333        destination_chain: ChainId,
334        destination_owner: OwnershipProof,
335        current_block_height: u64,
336        finality_depth: u64,
337    ) -> Result<CrossChainTransferResult, CrossChainError> {
338        // Step 1: Lock on source chain
339        let (lock_event, inclusion_proof) = locker.lock_right(
340            right_id,
341            commitment,
342            owner.clone(),
343            destination_chain.clone(),
344            destination_owner.clone(),
345        )?;
346
347        // Step 2: Build transfer proof
348        let source_chain = lock_event.source_chain.clone();
349        let source_block_height = lock_event.source_block_height;
350        let lock_timestamp = lock_event.timestamp;
351
352        let is_finalized = current_block_height >= source_block_height + finality_depth;
353
354        let transfer_proof = CrossChainTransferProof {
355            lock_event,
356            inclusion_proof,
357            finality_proof: CrossChainFinalityProof {
358                source_chain: source_chain.clone(),
359                height: source_block_height,
360                current_height: current_block_height,
361                is_finalized,
362                depth: finality_depth,
363            },
364            source_state_root: Hash::new([0u8; 32]),
365        };
366
367        // Step 3: Verify on destination
368        verifier.verify_transfer_proof(&transfer_proof)?;
369
370        // Step 4: Mint on destination
371        let result = minter.mint_right(&transfer_proof)?;
372
373        // Step 5: Record in registry
374        let entry = CrossChainRegistryEntry {
375            right_id,
376            source_chain,
377            source_seal: transfer_proof.lock_event.source_seal.clone(),
378            destination_chain: transfer_proof.lock_event.destination_chain.clone(),
379            destination_seal: result.destination_seal.clone(),
380            lock_tx_hash: transfer_proof.lock_event.source_tx_hash,
381            mint_tx_hash: Hash::new([0u8; 32]),
382            timestamp: lock_timestamp,
383        };
384        self.registry.record_transfer(entry)?;
385
386        Ok(result)
387    }
388}
389
390/// Cross-chain seal registry.
391///
392/// Tracks all cross-chain transfers to prevent double-spends.
393#[derive(Default)]
394pub struct CrossChainRegistry {
395    entries: alloc::collections::BTreeMap<Hash, CrossChainRegistryEntry>,
396}
397
398impl CrossChainRegistry {
399    /// Create a new empty registry.
400    pub fn new() -> Self {
401        Self {
402            entries: alloc::collections::BTreeMap::new(),
403        }
404    }
405
406    /// Record a cross-chain transfer.
407    pub fn record_transfer(
408        &mut self,
409        entry: CrossChainRegistryEntry,
410    ) -> Result<(), CrossChainError> {
411        // Check if this Right has already been transferred
412        if self.entries.contains_key(&entry.right_id) {
413            return Err(CrossChainError::AlreadyMinted);
414        }
415
416        // Check if the source seal has already been consumed
417        for existing in self.entries.values() {
418            if existing.source_seal == entry.source_seal {
419                return Err(CrossChainError::AlreadyLocked);
420            }
421        }
422
423        self.entries.insert(entry.right_id, entry);
424        Ok(())
425    }
426
427    /// Check if a Right has already been transferred.
428    pub fn is_right_transferred(&self, right_id: &Hash) -> bool {
429        self.entries.contains_key(right_id)
430    }
431
432    /// Check if a source seal has already been consumed.
433    pub fn is_seal_consumed(&self, seal: &SealRef) -> bool {
434        self.entries.values().any(|e| &e.source_seal == seal)
435    }
436
437    /// Get the registry entry for a Right.
438    pub fn get_entry(&self, right_id: &Hash) -> Option<&CrossChainRegistryEntry> {
439        self.entries.get(right_id)
440    }
441
442    /// Get the number of recorded transfers.
443    pub fn transfer_count(&self) -> usize {
444        self.entries.len()
445    }
446
447    /// Get all recorded transfers.
448    pub fn all_transfers(&self) -> Vec<&CrossChainRegistryEntry> {
449        self.entries.values().collect()
450    }
451}
452
453// Re-export for convenience
454pub use crate::seal_registry::SealConsumption;
455
456/// Cross-chain seal registry for tracking transfers across all chains
457pub use crate::seal_registry::CrossChainSealRegistry;
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::hash::Hash;
463
464    #[test]
465    fn test_chain_id_roundtrip() {
466        for chain in [
467            ChainId::Bitcoin,
468            ChainId::Sui,
469            ChainId::Aptos,
470            ChainId::Ethereum,
471        ] {
472            let id = chain.as_u8();
473            assert_eq!(ChainId::from_u8(id), Some(chain));
474        }
475        assert_eq!(ChainId::from_u8(99), None);
476    }
477
478    #[test]
479    fn test_registry_prevents_double_mint() {
480        let mut registry = CrossChainRegistry::new();
481        let right_id = Hash::new([0xAB; 32]);
482
483        let entry = CrossChainRegistryEntry {
484            right_id,
485            source_chain: ChainId::Bitcoin,
486            source_seal: SealRef::new(vec![0x01], None).unwrap(),
487            destination_chain: ChainId::Sui,
488            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
489            lock_tx_hash: Hash::new([0x03; 32]),
490            mint_tx_hash: Hash::new([0x04; 32]),
491            timestamp: 1_000_000,
492        };
493
494        registry.record_transfer(entry.clone()).unwrap();
495
496        // Second transfer of same Right should fail
497        let result = registry.record_transfer(entry);
498        assert!(matches!(result, Err(CrossChainError::AlreadyMinted)));
499    }
500
501    #[test]
502    fn test_registry_prevents_double_lock() {
503        let mut registry = CrossChainRegistry::new();
504        let seal = SealRef::new(vec![0x01], None).unwrap();
505
506        let entry1 = CrossChainRegistryEntry {
507            right_id: Hash::new([0xAB; 32]),
508            source_chain: ChainId::Bitcoin,
509            source_seal: seal.clone(),
510            destination_chain: ChainId::Sui,
511            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
512            lock_tx_hash: Hash::new([0x03; 32]),
513            mint_tx_hash: Hash::new([0x04; 32]),
514            timestamp: 1_000_000,
515        };
516
517        registry.record_transfer(entry1).unwrap();
518
519        // Second transfer using same source seal should fail
520        let entry2 = CrossChainRegistryEntry {
521            right_id: Hash::new([0xCD; 32]),
522            source_chain: ChainId::Bitcoin,
523            source_seal: seal.clone(),
524            destination_chain: ChainId::Aptos,
525            destination_seal: SealRef::new(vec![0x05], None).unwrap(),
526            lock_tx_hash: Hash::new([0x06; 32]),
527            mint_tx_hash: Hash::new([0x07; 32]),
528            timestamp: 2_000_000,
529        };
530
531        let result = registry.record_transfer(entry2);
532        assert!(matches!(result, Err(CrossChainError::AlreadyLocked)));
533    }
534
535    #[test]
536    fn test_registry_tracks_transfers() {
537        let mut registry = CrossChainRegistry::new();
538        assert_eq!(registry.transfer_count(), 0);
539
540        let entry = CrossChainRegistryEntry {
541            right_id: Hash::new([0xAB; 32]),
542            source_chain: ChainId::Bitcoin,
543            source_seal: SealRef::new(vec![0x01], None).unwrap(),
544            destination_chain: ChainId::Sui,
545            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
546            lock_tx_hash: Hash::new([0x03; 32]),
547            mint_tx_hash: Hash::new([0x04; 32]),
548            timestamp: 1_000_000,
549        };
550
551        registry.record_transfer(entry).unwrap();
552        assert_eq!(registry.transfer_count(), 1);
553        assert!(registry.is_right_transferred(&Hash::new([0xAB; 32])));
554    }
555}