Skip to main content

csv_adapter_core/
store.rs

1//! Persistent seal and anchor storage
2//!
3//! Provides a trait-based abstraction for persisting consumed seals,
4//! published anchors, and chain state across restarts.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8use serde::{Deserialize, Serialize};
9
10use crate::hash::Hash;
11
12/// A persisted seal record
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SealRecord {
15    /// Chain identifier (e.g., "bitcoin", "ethereum")
16    pub chain: String,
17    /// Seal identifier (chain-specific encoding)
18    pub seal_id: Vec<u8>,
19    /// Block height when the seal was consumed
20    pub consumed_at_height: u64,
21    /// Commitment hash that consumed this seal
22    pub commitment_hash: Hash,
23    /// Timestamp (Unix epoch seconds) when recorded
24    pub recorded_at: u64,
25}
26
27/// A persisted anchor record
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct AnchorRecord {
30    /// Chain identifier
31    pub chain: String,
32    /// Anchor identifier (chain-specific encoding)
33    pub anchor_id: Vec<u8>,
34    /// Block height where the anchor was included
35    pub block_height: u64,
36    /// Commitment hash that was anchored
37    pub commitment_hash: Hash,
38    /// Whether this anchor has been finalized
39    pub is_finalized: bool,
40    /// Number of confirmations at time of recording
41    pub confirmations: u64,
42    /// Timestamp (Unix epoch seconds)
43    pub recorded_at: u64,
44}
45
46/// Trait for persistent seal and anchor storage
47pub trait SealStore: Send + Sync {
48    /// Save a consumed seal record
49    fn save_seal(&mut self, record: &SealRecord) -> Result<(), StoreError>;
50
51    /// Check if a seal has been consumed
52    fn is_seal_consumed(&self, chain: &str, seal_id: &[u8]) -> Result<bool, StoreError>;
53
54    /// Get all consumed seals for a chain
55    fn get_seals(&self, chain: &str) -> Result<Vec<SealRecord>, StoreError>;
56
57    /// Remove a seal record (for reorg rollback)
58    fn remove_seal(&mut self, chain: &str, seal_id: &[u8]) -> Result<(), StoreError>;
59
60    /// Remove all seals consumed after a given height (reorg rollback)
61    fn remove_seals_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError>;
62
63    /// Save a published anchor record
64    fn save_anchor(&mut self, record: &AnchorRecord) -> Result<(), StoreError>;
65
66    /// Check if an anchor exists
67    fn has_anchor(&self, chain: &str, anchor_id: &[u8]) -> Result<bool, StoreError>;
68
69    /// Update anchor finalization status
70    fn finalize_anchor(
71        &mut self,
72        chain: &str,
73        anchor_id: &[u8],
74        confirmations: u64,
75    ) -> Result<(), StoreError>;
76
77    /// Get anchors that are not yet finalized
78    fn pending_anchors(&self, chain: &str) -> Result<Vec<AnchorRecord>, StoreError>;
79
80    /// Remove anchors published after a given height (reorg rollback)
81    fn remove_anchors_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError>;
82
83    /// Get the highest recorded block height for a chain
84    fn highest_block(&self, chain: &str) -> Result<u64, StoreError>;
85}
86
87/// In-memory store for testing and lightweight use
88pub struct InMemorySealStore {
89    seals: Vec<SealRecord>,
90    anchors: Vec<AnchorRecord>,
91}
92
93impl InMemorySealStore {
94    /// Create a new empty in-memory store
95    pub fn new() -> Self {
96        Self {
97            seals: Vec::new(),
98            anchors: Vec::new(),
99        }
100    }
101}
102
103impl Default for InMemorySealStore {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl SealStore for InMemorySealStore {
110    fn save_seal(&mut self, record: &SealRecord) -> Result<(), StoreError> {
111        self.seals.push(record.clone());
112        Ok(())
113    }
114
115    fn is_seal_consumed(&self, chain: &str, seal_id: &[u8]) -> Result<bool, StoreError> {
116        Ok(self
117            .seals
118            .iter()
119            .any(|s| s.chain == chain && s.seal_id == seal_id))
120    }
121
122    fn get_seals(&self, chain: &str) -> Result<Vec<SealRecord>, StoreError> {
123        Ok(self
124            .seals
125            .iter()
126            .filter(|s| s.chain == chain)
127            .cloned()
128            .collect())
129    }
130
131    fn remove_seal(&mut self, chain: &str, seal_id: &[u8]) -> Result<(), StoreError> {
132        self.seals
133            .retain(|s| !(s.chain == chain && s.seal_id == seal_id));
134        Ok(())
135    }
136
137    fn remove_seals_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError> {
138        let before = self.seals.len();
139        self.seals
140            .retain(|s| !(s.chain == chain && s.consumed_at_height > height));
141        Ok(before - self.seals.len())
142    }
143
144    fn save_anchor(&mut self, record: &AnchorRecord) -> Result<(), StoreError> {
145        self.anchors.push(record.clone());
146        Ok(())
147    }
148
149    fn has_anchor(&self, chain: &str, anchor_id: &[u8]) -> Result<bool, StoreError> {
150        Ok(self
151            .anchors
152            .iter()
153            .any(|a| a.chain == chain && a.anchor_id == anchor_id))
154    }
155
156    fn finalize_anchor(
157        &mut self,
158        chain: &str,
159        anchor_id: &[u8],
160        confirmations: u64,
161    ) -> Result<(), StoreError> {
162        if let Some(a) = self
163            .anchors
164            .iter_mut()
165            .find(|a| a.chain == chain && a.anchor_id == anchor_id)
166        {
167            a.is_finalized = true;
168            a.confirmations = confirmations;
169        }
170        Ok(())
171    }
172
173    fn pending_anchors(&self, chain: &str) -> Result<Vec<AnchorRecord>, StoreError> {
174        Ok(self
175            .anchors
176            .iter()
177            .filter(|a| a.chain == chain && !a.is_finalized)
178            .cloned()
179            .collect())
180    }
181
182    fn remove_anchors_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError> {
183        let before = self.anchors.len();
184        self.anchors
185            .retain(|a| !(a.chain == chain && a.block_height > height));
186        Ok(before - self.anchors.len())
187    }
188
189    fn highest_block(&self, chain: &str) -> Result<u64, StoreError> {
190        Ok(self
191            .anchors
192            .iter()
193            .filter(|a| a.chain == chain)
194            .map(|a| a.block_height)
195            .max()
196            .unwrap_or(0))
197    }
198}
199
200/// Store error types
201#[derive(Debug)]
202#[allow(missing_docs)]
203pub enum StoreError {
204    /// Database I/O error
205    IoError(String),
206    /// Serialization error
207    SerializationError(String),
208    /// Duplicate record
209    DuplicateRecord(String),
210    /// Record not found
211    NotFound(String),
212}
213
214impl core::fmt::Display for StoreError {
215    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
216        match self {
217            StoreError::IoError(msg) => write!(f, "I/O error: {}", msg),
218            StoreError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
219            StoreError::DuplicateRecord(msg) => write!(f, "Duplicate record: {}", msg),
220            StoreError::NotFound(msg) => write!(f, "Not found: {}", msg),
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    fn test_seal_record(chain: &str, height: u64) -> SealRecord {
230        SealRecord {
231            chain: chain.to_string(),
232            seal_id: vec![1, 2, 3],
233            consumed_at_height: height,
234            commitment_hash: Hash::new([0xAA; 32]),
235            recorded_at: 1700000000,
236        }
237    }
238
239    fn test_anchor_record(chain: &str, height: u64) -> AnchorRecord {
240        AnchorRecord {
241            chain: chain.to_string(),
242            anchor_id: vec![4, 5, 6],
243            block_height: height,
244            commitment_hash: Hash::new([0xBB; 32]),
245            is_finalized: false,
246            confirmations: 0,
247            recorded_at: 1700000000,
248        }
249    }
250
251    #[test]
252    fn test_store_seal_and_check() {
253        let mut store = InMemorySealStore::new();
254        let record = test_seal_record("bitcoin", 100);
255        store.save_seal(&record).unwrap();
256        assert!(store.is_seal_consumed("bitcoin", &[1, 2, 3]).unwrap());
257        assert!(!store.is_seal_consumed("ethereum", &[1, 2, 3]).unwrap());
258    }
259
260    #[test]
261    fn test_remove_seal() {
262        let mut store = InMemorySealStore::new();
263        store.save_seal(&test_seal_record("bitcoin", 100)).unwrap();
264        store.remove_seal("bitcoin", &[1, 2, 3]).unwrap();
265        assert!(!store.is_seal_consumed("bitcoin", &[1, 2, 3]).unwrap());
266    }
267
268    #[test]
269    fn test_remove_seals_after_height() {
270        let mut store = InMemorySealStore::new();
271        store.save_seal(&test_seal_record("bitcoin", 100)).unwrap();
272        store.save_seal(&test_seal_record("bitcoin", 150)).unwrap();
273        store.save_seal(&test_seal_record("bitcoin", 200)).unwrap();
274        let removed = store.remove_seals_after("bitcoin", 150).unwrap();
275        assert_eq!(removed, 1);
276        assert!(store.is_seal_consumed("bitcoin", &[1, 2, 3]).unwrap());
277    }
278
279    #[test]
280    fn test_anchor_lifecycle() {
281        let mut store = InMemorySealStore::new();
282        let anchor = test_anchor_record("bitcoin", 100);
283        store.save_anchor(&anchor).unwrap();
284        assert!(store.has_anchor("bitcoin", &[4, 5, 6]).unwrap());
285
286        // Initially not finalized
287        let pending = store.pending_anchors("bitcoin").unwrap();
288        assert_eq!(pending.len(), 1);
289
290        // Finalize
291        store.finalize_anchor("bitcoin", &[4, 5, 6], 6).unwrap();
292        let pending = store.pending_anchors("bitcoin").unwrap();
293        assert!(pending.is_empty());
294    }
295
296    #[test]
297    fn test_remove_anchors_after_height() {
298        let mut store = InMemorySealStore::new();
299        store
300            .save_anchor(&test_anchor_record("bitcoin", 100))
301            .unwrap();
302        store
303            .save_anchor(&test_anchor_record("bitcoin", 200))
304            .unwrap();
305        store
306            .save_anchor(&test_anchor_record("bitcoin", 300))
307            .unwrap();
308        let removed = store.remove_anchors_after("bitcoin", 200).unwrap();
309        assert_eq!(removed, 1);
310        assert!(store.has_anchor("bitcoin", &[4, 5, 6]).unwrap());
311    }
312
313    #[test]
314    fn test_highest_block() {
315        let mut store = InMemorySealStore::new();
316        store
317            .save_anchor(&test_anchor_record("bitcoin", 100))
318            .unwrap();
319        store
320            .save_anchor(&test_anchor_record("bitcoin", 300))
321            .unwrap();
322        store
323            .save_anchor(&test_anchor_record("bitcoin", 200))
324            .unwrap();
325        assert_eq!(store.highest_block("bitcoin").unwrap(), 300);
326        assert_eq!(store.highest_block("ethereum").unwrap(), 0);
327    }
328
329    #[test]
330    fn test_multi_chain_isolation() {
331        let mut store = InMemorySealStore::new();
332        store.save_seal(&test_seal_record("bitcoin", 100)).unwrap();
333        store.save_seal(&test_seal_record("ethereum", 200)).unwrap();
334
335        assert_eq!(store.get_seals("bitcoin").unwrap().len(), 1);
336        assert_eq!(store.get_seals("ethereum").unwrap().len(), 1);
337    }
338
339    #[test]
340    fn test_store_error_display() {
341        let err = StoreError::IoError("disk full".to_string());
342        assert!(err.to_string().contains("disk full"));
343    }
344}