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 #[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 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 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 verifier.verify_transfer_proof(&transfer_proof)?;
370
371 let result = minter.mint_right(&transfer_proof)?;
373
374 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#[derive(Default)]
395pub struct CrossChainRegistry {
396 entries: alloc::collections::BTreeMap<Hash, CrossChainRegistryEntry>,
397}
398
399impl CrossChainRegistry {
400 pub fn new() -> Self {
402 Self {
403 entries: alloc::collections::BTreeMap::new(),
404 }
405 }
406
407 pub fn record_transfer(
409 &mut self,
410 entry: CrossChainRegistryEntry,
411 ) -> Result<(), CrossChainError> {
412 if self.entries.contains_key(&entry.right_id) {
414 return Err(CrossChainError::AlreadyMinted);
415 }
416
417 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 pub fn is_right_transferred(&self, right_id: &Hash) -> bool {
430 self.entries.contains_key(right_id)
431 }
432
433 pub fn is_seal_consumed(&self, seal: &SealRef) -> bool {
435 self.entries.values().any(|e| &e.source_seal == seal)
436 }
437
438 pub fn get_entry(&self, right_id: &Hash) -> Option<&CrossChainRegistryEntry> {
440 self.entries.get(right_id)
441 }
442
443 pub fn transfer_count(&self) -> usize {
445 self.entries.len()
446 }
447
448 pub fn all_transfers(&self) -> Vec<&CrossChainRegistryEntry> {
450 self.entries.values().collect()
451 }
452}
453
454pub use crate::seal_registry::SealConsumption;
456
457pub 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 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 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}