use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::error::{SuiError, SuiResult};
use crate::types::SuiSealRef;
use csv_adapter_core::hardening::{BoundedQueue, MAX_SEAL_REGISTRY_SIZE};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SealRecord {
pub object_id: [u8; 32],
pub object_version: u64,
pub nonce: u64,
pub consumed_at_checkpoint: u64,
pub consumed_at: i64,
}
impl SealRecord {
pub fn new(seal: &SuiSealRef, consumed_at_checkpoint: u64) -> Self {
Self {
object_id: seal.object_id,
object_version: seal.version,
nonce: seal.nonce,
consumed_at_checkpoint,
consumed_at: chrono::Utc::now().timestamp(),
}
}
pub fn key(&self) -> String {
format!(
"{}-{}-{}",
hex::encode(self.object_id),
self.object_version,
self.nonce
)
}
}
#[derive(Debug)]
pub struct SealRegistry {
used_seals: HashSet<String>,
seal_records: Vec<SealRecord>,
seal_queue: BoundedQueue<String>,
max_size: usize,
}
impl SealRegistry {
pub fn new() -> Self {
Self::with_max_size(MAX_SEAL_REGISTRY_SIZE)
}
pub fn with_max_size(max_size: usize) -> Self {
Self {
used_seals: HashSet::new(),
seal_records: Vec::new(),
seal_queue: BoundedQueue::new(max_size),
max_size,
}
}
pub fn is_seal_used(&self, seal: &SuiSealRef) -> bool {
let key = format!(
"{}-{}-{}",
hex::encode(seal.object_id),
seal.version,
seal.nonce
);
self.used_seals.contains(&key)
}
pub fn mark_seal_used(
&mut self,
seal: &SuiSealRef,
consumed_at_checkpoint: u64,
) -> SuiResult<()> {
if self.is_seal_used(seal) {
return Err(SuiError::ObjectUsed(format!(
"Object 0x{} with version {} is already consumed",
hex::encode(seal.object_id),
seal.version
)));
}
if self.used_seals.len() >= self.max_size {
return Err(SuiError::ObjectUsed(format!(
"Seal registry is full (max {} entries)",
self.max_size
)));
}
let record = SealRecord::new(seal, consumed_at_checkpoint);
let key = record.key();
self.seal_queue.push(key.clone());
self.used_seals.insert(key);
self.seal_records.push(record);
Ok(())
}
pub fn clear_seal(&mut self, seal: &SuiSealRef) -> SuiResult<()> {
let key = format!(
"{}-{}-{}",
hex::encode(seal.object_id),
seal.version,
seal.nonce
);
if !self.used_seals.remove(&key) {
return Err(SuiError::ObjectUsed(format!(
"Seal 0x{} not found in registry",
hex::encode(seal.object_id)
)));
}
self.seal_records.retain(|r| r.key() != key);
Ok(())
}
pub fn len(&self) -> usize {
self.used_seals.len()
}
pub fn is_empty(&self) -> bool {
self.used_seals.is_empty()
}
pub fn is_full(&self) -> bool {
self.used_seals.len() >= self.max_size
}
pub fn max_size(&self) -> usize {
self.max_size
}
pub fn records(&self) -> &[SealRecord] {
&self.seal_records
}
#[cfg(test)]
pub fn clear(&mut self) {
self.used_seals.clear();
self.seal_records.clear();
}
pub fn export_records(&self) -> Vec<SealRecord> {
self.seal_records.clone()
}
pub fn import_records(&mut self, records: Vec<SealRecord>) {
for record in &records {
self.used_seals.insert(record.key());
}
self.seal_records = records;
}
}
impl Default for SealRegistry {
fn default() -> Self {
Self::new()
}
}
pub trait SealStore: Send + Sync {
fn load_seals(&self) -> Result<Vec<SealRecord>, Box<dyn std::error::Error + Send + Sync>>;
fn save_seals(
&self,
records: &[SealRecord],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
#[cfg(test)]
mod tests {
use super::*;
fn test_seal() -> SuiSealRef {
SuiSealRef::new([1u8; 32], 1, 0)
}
#[test]
fn test_new_registry_is_empty() {
let registry = SealRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_mark_seal_used() {
let mut registry = SealRegistry::new();
let seal = test_seal();
assert!(!registry.is_seal_used(&seal));
registry.mark_seal_used(&seal, 100).unwrap();
assert!(registry.is_seal_used(&seal));
assert_eq!(registry.len(), 1);
}
#[test]
fn test_mark_seal_used_replay_prevention() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
let result = registry.mark_seal_used(&seal, 200);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SuiError::ObjectUsed(_)));
}
#[test]
fn test_different_seals() {
let mut registry = SealRegistry::new();
let seal1 = SuiSealRef::new([1u8; 32], 1, 0);
let seal2 = SuiSealRef::new([2u8; 32], 1, 0);
registry.mark_seal_used(&seal1, 100).unwrap();
registry.mark_seal_used(&seal2, 200).unwrap();
assert!(registry.is_seal_used(&seal1));
assert!(registry.is_seal_used(&seal2));
assert_eq!(registry.len(), 2);
}
#[test]
fn test_same_object_different_version() {
let mut registry = SealRegistry::new();
let seal1 = SuiSealRef::new([1u8; 32], 1, 0);
let seal2 = SuiSealRef::new([1u8; 32], 2, 0);
registry.mark_seal_used(&seal1, 100).unwrap();
registry.mark_seal_used(&seal2, 200).unwrap();
assert!(registry.is_seal_used(&seal1));
assert!(registry.is_seal_used(&seal2));
assert_eq!(registry.len(), 2);
}
#[test]
fn test_export_import() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
let records = registry.export_records();
let mut new_registry = SealRegistry::new();
new_registry.import_records(records);
assert!(new_registry.is_seal_used(&seal));
assert_eq!(new_registry.len(), 1);
}
#[test]
fn test_seal_record_serialization() {
let seal = test_seal();
let record = SealRecord::new(&seal, 100);
let json = serde_json::to_string(&record).unwrap();
let deserialized: SealRecord = serde_json::from_str(&json).unwrap();
assert_eq!(record, deserialized);
}
#[test]
fn test_clear_registry() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
registry.clear();
assert!(!registry.is_seal_used(&seal));
assert!(registry.is_empty());
}
}