1use alloc::vec::Vec;
10use serde::{Deserialize, Serialize};
11
12use crate::hash::Hash;
13use crate::right::{OwnershipProof, Right};
14use crate::seal::SealRef;
15
16#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[allow(missing_docs)]
19pub enum ChainId {
20 Bitcoin,
22 Sui,
24 Aptos,
26 Ethereum,
28}
29
30impl ChainId {
31 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 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct CrossChainLockEvent {
67 pub right_id: Hash,
69 pub commitment: Hash,
71 pub owner: OwnershipProof,
73 pub source_chain: ChainId,
75 pub destination_chain: ChainId,
77 pub destination_owner: OwnershipProof,
79 pub source_seal: SealRef,
81 pub source_tx_hash: Hash,
83 pub source_block_height: u64,
85 pub timestamp: u64,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub enum InclusionProof {
92 Bitcoin(BitcoinMerkleProof),
94 Ethereum(EthereumMPTProof),
96 Sui(SuiCheckpointProof),
98 Aptos(AptosLedgerProof),
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104#[allow(missing_docs)]
105pub struct BitcoinMerkleProof {
106 pub txid: [u8; 32],
108 pub merkle_branch: Vec<[u8; 32]>,
110 pub block_header: Vec<u8>,
112 pub block_height: u64,
114 pub confirmations: u64,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120#[allow(missing_docs)]
121pub struct EthereumMPTProof {
122 pub tx_hash: [u8; 32],
124 pub receipt_root: [u8; 32],
126 pub receipt_rlp: Vec<u8>,
128 pub merkle_nodes: Vec<Vec<u8>>,
130 pub block_header: Vec<u8>,
132 pub log_index: u64,
134 pub confirmations: u64,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140#[allow(missing_docs)]
141pub struct SuiCheckpointProof {
142 pub tx_digest: [u8; 32],
144 pub checkpoint_sequence: u64,
146 pub checkpoint_contents_hash: [u8; 32],
148 pub effects: Vec<u8>,
150 pub events: Vec<u8>,
152 pub certified: bool,
154}
155
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158#[allow(missing_docs)]
159pub struct AptosLedgerProof {
160 pub version: u64,
162 pub transaction_proof: Vec<u8>,
164 pub ledger_info: Vec<u8>,
166 pub events: Vec<u8>,
168 pub success: bool,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
174pub struct CrossChainFinalityProof {
175 pub source_chain: ChainId,
177 pub height: u64,
179 pub current_height: u64,
181 pub is_finalized: bool,
183 pub depth: u64,
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189pub struct CrossChainTransferProof {
190 pub lock_event: CrossChainLockEvent,
192 pub inclusion_proof: InclusionProof,
194 pub finality_proof: CrossChainFinalityProof,
196 pub source_state_root: Hash,
198}
199
200#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct CrossChainRegistryEntry {
203 pub right_id: Hash,
205 pub source_chain: ChainId,
207 pub source_seal: SealRef,
209 pub destination_chain: ChainId,
211 pub destination_seal: SealRef,
213 pub lock_tx_hash: Hash,
215 pub mint_tx_hash: Hash,
217 pub timestamp: u64,
219}
220
221#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct CrossChainTransferResult {
224 pub destination_right: Right,
226 pub destination_seal: SealRef,
228 pub registry_entry: CrossChainRegistryEntry,
230}
231
232#[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
254pub trait LockProvider {
258 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
279pub trait TransferVerifier {
281 fn verify_transfer_proof(&self, proof: &CrossChainTransferProof)
289 -> Result<(), CrossChainError>;
290}
291
292pub trait MintProvider {
294 fn mint_right(
299 &self,
300 proof: &CrossChainTransferProof,
301 ) -> Result<CrossChainTransferResult, CrossChainError>;
302}
303
304pub struct CrossChainTransfer {
308 pub registry: CrossChainRegistry,
310}
311
312impl CrossChainTransfer {
313 pub fn new(registry: CrossChainRegistry) -> Self {
315 Self { registry }
316 }
317
318 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 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 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 verifier.verify_transfer_proof(&transfer_proof)?;
369
370 let result = minter.mint_right(&transfer_proof)?;
372
373 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#[derive(Default)]
394pub struct CrossChainRegistry {
395 entries: alloc::collections::BTreeMap<Hash, CrossChainRegistryEntry>,
396}
397
398impl CrossChainRegistry {
399 pub fn new() -> Self {
401 Self {
402 entries: alloc::collections::BTreeMap::new(),
403 }
404 }
405
406 pub fn record_transfer(
408 &mut self,
409 entry: CrossChainRegistryEntry,
410 ) -> Result<(), CrossChainError> {
411 if self.entries.contains_key(&entry.right_id) {
413 return Err(CrossChainError::AlreadyMinted);
414 }
415
416 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 pub fn is_right_transferred(&self, right_id: &Hash) -> bool {
429 self.entries.contains_key(right_id)
430 }
431
432 pub fn is_seal_consumed(&self, seal: &SealRef) -> bool {
434 self.entries.values().any(|e| &e.source_seal == seal)
435 }
436
437 pub fn get_entry(&self, right_id: &Hash) -> Option<&CrossChainRegistryEntry> {
439 self.entries.get(right_id)
440 }
441
442 pub fn transfer_count(&self) -> usize {
444 self.entries.len()
445 }
446
447 pub fn all_transfers(&self) -> Vec<&CrossChainRegistryEntry> {
449 self.entries.values().collect()
450 }
451}
452
453pub use crate::seal_registry::SealConsumption;
455
456pub 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 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 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}