1use alloc::string::String;
7use alloc::vec::Vec;
8use serde::{Deserialize, Serialize};
9
10use crate::hash::Hash;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SealRecord {
15 pub chain: String,
17 pub seal_id: Vec<u8>,
19 pub consumed_at_height: u64,
21 pub commitment_hash: Hash,
23 pub recorded_at: u64,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct AnchorRecord {
30 pub chain: String,
32 pub anchor_id: Vec<u8>,
34 pub block_height: u64,
36 pub commitment_hash: Hash,
38 pub is_finalized: bool,
40 pub confirmations: u64,
42 pub recorded_at: u64,
44}
45
46pub trait SealStore: Send + Sync {
48 fn save_seal(&mut self, record: &SealRecord) -> Result<(), StoreError>;
50
51 fn is_seal_consumed(&self, chain: &str, seal_id: &[u8]) -> Result<bool, StoreError>;
53
54 fn get_seals(&self, chain: &str) -> Result<Vec<SealRecord>, StoreError>;
56
57 fn remove_seal(&mut self, chain: &str, seal_id: &[u8]) -> Result<(), StoreError>;
59
60 fn remove_seals_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError>;
62
63 fn save_anchor(&mut self, record: &AnchorRecord) -> Result<(), StoreError>;
65
66 fn has_anchor(&self, chain: &str, anchor_id: &[u8]) -> Result<bool, StoreError>;
68
69 fn finalize_anchor(
71 &mut self,
72 chain: &str,
73 anchor_id: &[u8],
74 confirmations: u64,
75 ) -> Result<(), StoreError>;
76
77 fn pending_anchors(&self, chain: &str) -> Result<Vec<AnchorRecord>, StoreError>;
79
80 fn remove_anchors_after(&mut self, chain: &str, height: u64) -> Result<usize, StoreError>;
82
83 fn highest_block(&self, chain: &str) -> Result<u64, StoreError>;
85}
86
87pub struct InMemorySealStore {
89 seals: Vec<SealRecord>,
90 anchors: Vec<AnchorRecord>,
91}
92
93impl InMemorySealStore {
94 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#[derive(Debug)]
202#[allow(missing_docs)]
203pub enum StoreError {
204 IoError(String),
206 SerializationError(String),
208 DuplicateRecord(String),
210 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 let pending = store.pending_anchors("bitcoin").unwrap();
288 assert_eq!(pending.len(), 1);
289
290 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}