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    #[allow(clippy::too_many_arguments)]
326    pub fn execute(
327        &mut self,
328        locker: &dyn LockProvider,
329        verifier: &dyn TransferVerifier,
330        minter: &dyn MintProvider,
331        right_id: Hash,
332        commitment: Hash,
333        owner: OwnershipProof,
334        destination_chain: ChainId,
335        destination_owner: OwnershipProof,
336        current_block_height: u64,
337        finality_depth: u64,
338    ) -> Result<CrossChainTransferResult, CrossChainError> {
339        // Step 1: Lock on source chain
340        let (lock_event, inclusion_proof) = locker.lock_right(
341            right_id,
342            commitment,
343            owner.clone(),
344            destination_chain.clone(),
345            destination_owner.clone(),
346        )?;
347
348        // Step 2: Build transfer proof
349        let source_chain = lock_event.source_chain.clone();
350        let source_block_height = lock_event.source_block_height;
351        let lock_timestamp = lock_event.timestamp;
352
353        let is_finalized = current_block_height >= source_block_height + finality_depth;
354
355        let transfer_proof = CrossChainTransferProof {
356            lock_event,
357            inclusion_proof,
358            finality_proof: CrossChainFinalityProof {
359                source_chain: source_chain.clone(),
360                height: source_block_height,
361                current_height: current_block_height,
362                is_finalized,
363                depth: finality_depth,
364            },
365            source_state_root: Hash::new([0u8; 32]),
366        };
367
368        // Step 3: Verify on destination
369        verifier.verify_transfer_proof(&transfer_proof)?;
370
371        // Step 4: Mint on destination
372        let result = minter.mint_right(&transfer_proof)?;
373
374        // Step 5: Record in registry
375        let entry = CrossChainRegistryEntry {
376            right_id,
377            source_chain,
378            source_seal: transfer_proof.lock_event.source_seal.clone(),
379            destination_chain: transfer_proof.lock_event.destination_chain.clone(),
380            destination_seal: result.destination_seal.clone(),
381            lock_tx_hash: transfer_proof.lock_event.source_tx_hash,
382            mint_tx_hash: Hash::new([0u8; 32]),
383            timestamp: lock_timestamp,
384        };
385        self.registry.record_transfer(entry)?;
386
387        Ok(result)
388    }
389}
390
391/// Cross-chain seal registry.
392///
393/// Tracks all cross-chain transfers to prevent double-spends.
394#[derive(Default)]
395pub struct CrossChainRegistry {
396    entries: alloc::collections::BTreeMap<Hash, CrossChainRegistryEntry>,
397}
398
399impl CrossChainRegistry {
400    /// Create a new empty registry.
401    pub fn new() -> Self {
402        Self {
403            entries: alloc::collections::BTreeMap::new(),
404        }
405    }
406
407    /// Record a cross-chain transfer.
408    pub fn record_transfer(
409        &mut self,
410        entry: CrossChainRegistryEntry,
411    ) -> Result<(), CrossChainError> {
412        // Check if this Right has already been transferred
413        if self.entries.contains_key(&entry.right_id) {
414            return Err(CrossChainError::AlreadyMinted);
415        }
416
417        // Check if the source seal has already been consumed
418        for existing in self.entries.values() {
419            if existing.source_seal == entry.source_seal {
420                return Err(CrossChainError::AlreadyLocked);
421            }
422        }
423
424        self.entries.insert(entry.right_id, entry);
425        Ok(())
426    }
427
428    /// Check if a Right has already been transferred.
429    pub fn is_right_transferred(&self, right_id: &Hash) -> bool {
430        self.entries.contains_key(right_id)
431    }
432
433    /// Check if a source seal has already been consumed.
434    pub fn is_seal_consumed(&self, seal: &SealRef) -> bool {
435        self.entries.values().any(|e| &e.source_seal == seal)
436    }
437
438    /// Get the registry entry for a Right.
439    pub fn get_entry(&self, right_id: &Hash) -> Option<&CrossChainRegistryEntry> {
440        self.entries.get(right_id)
441    }
442
443    /// Get the number of recorded transfers.
444    pub fn transfer_count(&self) -> usize {
445        self.entries.len()
446    }
447
448    /// Get all recorded transfers.
449    pub fn all_transfers(&self) -> Vec<&CrossChainRegistryEntry> {
450        self.entries.values().collect()
451    }
452}
453
454// Re-export for convenience
455pub use crate::seal_registry::SealConsumption;
456
457/// Cross-chain seal registry for tracking transfers across all chains
458pub use crate::seal_registry::CrossChainSealRegistry;
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use crate::hash::Hash;
464
465    #[test]
466    fn test_chain_id_roundtrip() {
467        for chain in [
468            ChainId::Bitcoin,
469            ChainId::Sui,
470            ChainId::Aptos,
471            ChainId::Ethereum,
472        ] {
473            let id = chain.as_u8();
474            assert_eq!(ChainId::from_u8(id), Some(chain));
475        }
476        assert_eq!(ChainId::from_u8(99), None);
477    }
478
479    #[test]
480    fn test_registry_prevents_double_mint() {
481        let mut registry = CrossChainRegistry::new();
482        let right_id = Hash::new([0xAB; 32]);
483
484        let entry = CrossChainRegistryEntry {
485            right_id,
486            source_chain: ChainId::Bitcoin,
487            source_seal: SealRef::new(vec![0x01], None).unwrap(),
488            destination_chain: ChainId::Sui,
489            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
490            lock_tx_hash: Hash::new([0x03; 32]),
491            mint_tx_hash: Hash::new([0x04; 32]),
492            timestamp: 1_000_000,
493        };
494
495        registry.record_transfer(entry.clone()).unwrap();
496
497        // Second transfer of same Right should fail
498        let result = registry.record_transfer(entry);
499        assert!(matches!(result, Err(CrossChainError::AlreadyMinted)));
500    }
501
502    #[test]
503    fn test_registry_prevents_double_lock() {
504        let mut registry = CrossChainRegistry::new();
505        let seal = SealRef::new(vec![0x01], None).unwrap();
506
507        let entry1 = CrossChainRegistryEntry {
508            right_id: Hash::new([0xAB; 32]),
509            source_chain: ChainId::Bitcoin,
510            source_seal: seal.clone(),
511            destination_chain: ChainId::Sui,
512            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
513            lock_tx_hash: Hash::new([0x03; 32]),
514            mint_tx_hash: Hash::new([0x04; 32]),
515            timestamp: 1_000_000,
516        };
517
518        registry.record_transfer(entry1).unwrap();
519
520        // Second transfer using same source seal should fail
521        let entry2 = CrossChainRegistryEntry {
522            right_id: Hash::new([0xCD; 32]),
523            source_chain: ChainId::Bitcoin,
524            source_seal: seal.clone(),
525            destination_chain: ChainId::Aptos,
526            destination_seal: SealRef::new(vec![0x05], None).unwrap(),
527            lock_tx_hash: Hash::new([0x06; 32]),
528            mint_tx_hash: Hash::new([0x07; 32]),
529            timestamp: 2_000_000,
530        };
531
532        let result = registry.record_transfer(entry2);
533        assert!(matches!(result, Err(CrossChainError::AlreadyLocked)));
534    }
535
536    #[test]
537    fn test_registry_tracks_transfers() {
538        let mut registry = CrossChainRegistry::new();
539        assert_eq!(registry.transfer_count(), 0);
540
541        let entry = CrossChainRegistryEntry {
542            right_id: Hash::new([0xAB; 32]),
543            source_chain: ChainId::Bitcoin,
544            source_seal: SealRef::new(vec![0x01], None).unwrap(),
545            destination_chain: ChainId::Sui,
546            destination_seal: SealRef::new(vec![0x02], None).unwrap(),
547            lock_tx_hash: Hash::new([0x03; 32]),
548            mint_tx_hash: Hash::new([0x04; 32]),
549            timestamp: 1_000_000,
550        };
551
552        registry.record_transfer(entry).unwrap();
553        assert_eq!(registry.transfer_count(), 1);
554        assert!(registry.is_right_transferred(&Hash::new([0xAB; 32])));
555    }
556}