use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use crate::hash::Hash;
use crate::right::RightId;
use crate::seal::SealRef;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum ChainId {
Bitcoin,
Sui,
Aptos,
Ethereum,
Custom(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SealConsumption {
pub chain: ChainId,
pub seal_ref: SealRef,
pub right_id: RightId,
pub block_height: u64,
pub tx_hash: Hash,
pub recorded_at: u64,
}
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub enum SealStatus {
Unconsumed,
ConsumedOnChain {
chain: ChainId,
consumption: SealConsumption,
},
DoubleSpent { consumptions: Vec<SealConsumption> },
}
#[derive(Default)]
pub struct CrossChainSealRegistry {
consumed_seals: BTreeMap<Vec<u8>, Vec<SealConsumption>>,
right_consumption_map: BTreeMap<Hash, Vec<SealConsumption>>,
known_chains: BTreeSet<ChainId>,
}
impl CrossChainSealRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn record_consumption(
&mut self,
consumption: SealConsumption,
) -> Result<(), Box<DoubleSpendError>> {
let seal_key = consumption.seal_ref.to_vec();
let is_double_spend = self.consumed_seals.contains_key(&seal_key)
&& !self
.consumed_seals
.get(&seal_key)
.map_or(true, |v| v.is_empty());
self.known_chains.insert(consumption.chain.clone());
if is_double_spend {
let existing = self.consumed_seals.get(&seal_key).unwrap();
let is_cross_chain = existing.iter().any(|e| e.chain != consumption.chain);
let err = DoubleSpendError {
seal_ref: consumption.seal_ref.clone(),
existing_consumptions: existing.clone(),
new_consumption: consumption.clone(),
is_cross_chain,
};
self.consumed_seals
.entry(seal_key)
.or_default()
.push(consumption.clone());
self.right_consumption_map
.entry(consumption.right_id.0)
.or_default()
.push(consumption);
return Err(Box::new(err));
}
self.consumed_seals
.entry(seal_key)
.or_default()
.push(consumption.clone());
self.right_consumption_map
.entry(consumption.right_id.0)
.or_default()
.push(consumption);
Ok(())
}
pub fn check_seal_status(&self, seal_ref: &SealRef) -> SealStatus {
let key = seal_ref.to_vec();
match self.consumed_seals.get(&key) {
None => SealStatus::Unconsumed,
Some(consumptions) if consumptions.len() == 1 => {
let c = &consumptions[0];
SealStatus::ConsumedOnChain {
chain: c.chain.clone(),
consumption: c.clone(),
}
}
Some(consumptions) => SealStatus::DoubleSpent {
consumptions: consumptions.clone(),
},
}
}
pub fn is_seal_consumed(&self, seal_ref: &SealRef) -> bool {
self.consumed_seals.contains_key(&seal_ref.to_vec())
}
pub fn get_consumption_history(&self, seal_ref: &SealRef) -> Vec<SealConsumption> {
self.consumed_seals
.get(&seal_ref.to_vec())
.cloned()
.unwrap_or_default()
}
pub fn get_seals_for_right(&self, right_id: &RightId) -> Vec<SealConsumption> {
self.right_consumption_map
.get(&right_id.0)
.cloned()
.unwrap_or_default()
}
pub fn known_chains(&self) -> Vec<&ChainId> {
self.known_chains.iter().collect()
}
pub fn total_seals(&self) -> usize {
self.consumed_seals.len()
}
pub fn double_spend_count(&self) -> usize {
self.consumed_seals
.values()
.filter(|consumptions| consumptions.len() > 1)
.count()
}
}
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub struct DoubleSpendError {
pub seal_ref: SealRef,
pub existing_consumptions: Vec<SealConsumption>,
pub new_consumption: SealConsumption,
pub is_cross_chain: bool,
}
impl core::fmt::Display for DoubleSpendError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.is_cross_chain {
write!(
f,
"Cross-chain double-spend detected for seal {:?}",
self.seal_ref
)
} else {
write!(f, "Same-chain replay detected for seal {:?}", self.seal_ref)
}
}
}
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests {
use super::*;
fn make_consumption(chain: ChainId, seal_bytes: Vec<u8>, right_id: RightId) -> SealConsumption {
SealConsumption {
chain,
seal_ref: SealRef::new(seal_bytes, None).unwrap(),
right_id,
block_height: 100,
tx_hash: Hash::new([0xAB; 32]),
recorded_at: 1_000_000,
}
}
#[test]
fn test_record_single_consumption() {
let mut registry = CrossChainSealRegistry::new();
let right_id = RightId(Hash::new([0xCD; 32]));
let consumption = make_consumption(ChainId::Bitcoin, vec![0x01], right_id);
assert!(registry.record_consumption(consumption).is_ok());
assert_eq!(registry.total_seals(), 1);
assert_eq!(registry.double_spend_count(), 0);
}
#[test]
fn test_detect_same_chain_replay() {
let mut registry = CrossChainSealRegistry::new();
let right_id = RightId(Hash::new([0xCD; 32]));
let seal_bytes = vec![0x01];
let consumption1 = make_consumption(ChainId::Bitcoin, seal_bytes.clone(), right_id);
registry.record_consumption(consumption1).unwrap();
let right_id2 = RightId(Hash::new([0xEF; 32]));
let consumption2 = make_consumption(ChainId::Bitcoin, seal_bytes, right_id2);
let result = registry.record_consumption(consumption2);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(!err.is_cross_chain);
}
#[test]
fn test_detect_cross_chain_double_spend() {
let mut registry = CrossChainSealRegistry::new();
let right_id = RightId(Hash::new([0xCD; 32]));
let seal_bytes = vec![0x01];
let consumption1 = make_consumption(ChainId::Bitcoin, seal_bytes.clone(), right_id.clone());
registry.record_consumption(consumption1).unwrap();
let consumption2 = make_consumption(ChainId::Ethereum, seal_bytes, right_id);
let result = registry.record_consumption(consumption2);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_cross_chain);
assert_eq!(err.existing_consumptions.len(), 1);
}
#[test]
fn test_seal_status_unconsumed() {
let registry = CrossChainSealRegistry::new();
let seal = SealRef::new(vec![0x01], None).unwrap();
assert!(matches!(
registry.check_seal_status(&seal),
SealStatus::Unconsumed
));
}
#[test]
fn test_seal_status_consumed() {
let mut registry = CrossChainSealRegistry::new();
let right_id = RightId(Hash::new([0xCD; 32]));
let seal = SealRef::new(vec![0x01], None).unwrap();
let consumption = make_consumption(ChainId::Bitcoin, vec![0x01], right_id);
registry.record_consumption(consumption).unwrap();
match registry.check_seal_status(&seal) {
SealStatus::ConsumedOnChain { chain, .. } => {
assert_eq!(chain, ChainId::Bitcoin);
}
_ => panic!("Expected ConsumedOnChain"),
}
}
#[test]
fn test_seal_status_double_spent() {
let mut registry = CrossChainSealRegistry::new();
let right_id = RightId(Hash::new([0xCD; 32]));
let seal = SealRef::new(vec![0x01], None).unwrap();
let seal_bytes = vec![0x01];
let c1 = make_consumption(ChainId::Bitcoin, seal_bytes.clone(), right_id.clone());
registry.record_consumption(c1).unwrap();
let c2 = make_consumption(ChainId::Ethereum, seal_bytes, right_id.clone());
let _ = registry.record_consumption(c2);
assert!(matches!(
registry.check_seal_status(&seal),
SealStatus::DoubleSpent { .. }
));
}
#[test]
fn test_known_chains() {
let mut registry = CrossChainSealRegistry::new();
assert_eq!(registry.known_chains().len(), 0);
let right_id = RightId(Hash::new([0xCD; 32]));
let c1 = make_consumption(ChainId::Bitcoin, vec![0x01], right_id.clone());
registry.record_consumption(c1).unwrap();
assert_eq!(registry.known_chains().len(), 1);
}
}