1use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use corium_core::{Datom, IndexOrder, encoding::DecodeError};
13use corium_crypt::{Keyring, SecretKey, decrypt_blob, parse_blob_header};
14use corium_db::Db;
15use corium_protocol::codec::{self, CodecError};
16use corium_store::{
17 BlobId, BlobStore, DbRoot, DiscoveredStore, FORMAT_VERSION, KeyManifest, RootStore,
18 SegmentCache, SegmentCacheConfig, SegmentCacheMetrics, SegmentReader, StoreError, db_root_name,
19 decode_index_manifest, decode_segment_keys, digest, is_index_manifest, keys_root_name,
20 meta_root_name,
21};
22use thiserror::Error;
23
24#[async_trait]
29pub trait PeerStorage: Send + Sync {
30 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
32 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
34}
35
36pub struct CachedPeerStorage {
38 storage: Arc<dyn PeerStorage>,
39 cache: SegmentCache,
40}
41
42impl CachedPeerStorage {
43 pub fn open(
49 storage: Arc<dyn PeerStorage>,
50 config: &SegmentCacheConfig,
51 ) -> std::io::Result<Self> {
52 Ok(Self {
53 storage,
54 cache: SegmentCache::open(config)?,
55 })
56 }
57
58 #[must_use]
60 pub fn metrics(&self) -> Arc<SegmentCacheMetrics> {
61 self.cache.metrics()
62 }
63}
64
65#[async_trait]
66impl SegmentReader for CachedPeerStorage {
67 async fn read_segment(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
68 self.storage.get_blob(id).await
69 }
70}
71
72#[async_trait]
73impl PeerStorage for CachedPeerStorage {
74 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
75 Ok(self
76 .cache
77 .get_or_load(self, id)
78 .await?
79 .map(|bytes| bytes.to_vec()))
80 }
81 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
82 self.storage.get_peer_root(name).await
83 }
84}
85
86#[async_trait]
87impl<S> PeerStorage for S
88where
89 S: BlobStore + RootStore + Send + Sync,
90{
91 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
92 BlobStore::get(self, id).await
93 }
94
95 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
96 RootStore::get_root(self, name).await
97 }
98}
99
100pub struct EncryptedPeerStorage {
108 storage: Arc<dyn PeerStorage>,
109 keys: BTreeMap<u32, SecretKey>,
110}
111
112impl EncryptedPeerStorage {
113 #[must_use]
119 pub fn new(storage: Arc<dyn PeerStorage>, keys: BTreeMap<u32, SecretKey>) -> Self {
120 Self { storage, keys }
121 }
122}
123
124#[async_trait]
125impl PeerStorage for EncryptedPeerStorage {
126 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
127 let Some(object) = self.storage.get_blob(id).await? else {
128 return Ok(None);
129 };
130 if digest(&object) != *id {
131 return Err(StoreError::CorruptBlob(id.clone()));
132 }
133 let header = parse_blob_header(&object)?;
134 let key = self
135 .keys
136 .get(&header.epoch)
137 .ok_or(StoreError::MissingEncryptionKey(header.epoch))?;
138 Ok(Some(decrypt_blob(key, &object)?))
139 }
140
141 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
142 self.storage.get_peer_root(name).await
143 }
144}
145
146pub async fn open_encrypted_storage(
156 storage: Arc<dyn PeerStorage>,
157 db: &str,
158 keyring: Option<&Arc<dyn Keyring>>,
159) -> Result<Arc<dyn PeerStorage>, StoreError> {
160 let Some(bytes) = storage.get_peer_root(&keys_root_name(db)).await? else {
161 return Ok(storage);
162 };
163 let manifest = KeyManifest::decode(&bytes)?;
164 if manifest.storage_keys.is_empty() {
165 return Ok(storage);
166 }
167 let Some(keyring) = keyring else {
168 return Err(StoreError::EncryptedWithoutKey {
169 db: db.to_owned(),
170 kek: manifest.kek.to_string(),
171 });
172 };
173 let keys = manifest.unwrap_storage_keys(keyring.as_ref()).await?;
174 Ok(Arc::new(EncryptedPeerStorage::new(storage, keys)))
175}
176
177pub struct DiscoveredPeerStorage(DiscoveredStore);
179
180impl DiscoveredPeerStorage {
181 #[must_use]
183 pub const fn new(store: DiscoveredStore) -> Self {
184 Self(store)
185 }
186}
187
188#[async_trait]
189impl PeerStorage for DiscoveredPeerStorage {
190 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
191 self.0.get(id).await
192 }
193
194 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
195 self.0.get_root(name).await
196 }
197}
198
199#[derive(Debug, Error)]
201pub enum SnapshotError {
202 #[error(transparent)]
204 Store(#[from] StoreError),
205 #[error(transparent)]
207 Codec(#[from] CodecError),
208 #[error(transparent)]
210 Key(#[from] DecodeError),
211 #[error("malformed published root for database {0:?}")]
213 MalformedRoot(String),
214 #[error("storage format {found} is newer than supported format {supported}")]
216 UnsupportedFormat {
217 found: u32,
219 supported: u32,
221 },
222 #[error("published snapshot for database {0:?} has no metadata root")]
224 MissingMetadata(String),
225}
226
227pub async fn load_current_snapshot(
236 store: &dyn PeerStorage,
237 db: &str,
238) -> Result<Option<Db>, SnapshotError> {
239 let Some(root_bytes) = store.get_peer_root(&db_root_name(db)).await? else {
240 return Ok(None);
241 };
242 let root =
243 DbRoot::decode(&root_bytes).ok_or_else(|| SnapshotError::MalformedRoot(db.into()))?;
244 if root.format_version > FORMAT_VERSION {
245 return Err(SnapshotError::UnsupportedFormat {
246 found: root.format_version,
247 supported: FORMAT_VERSION,
248 });
249 }
250 let Some(roots) = root.roots else {
251 return Ok(None);
252 };
253 let Some(metadata) = store.get_peer_root(&meta_root_name(db)).await? else {
254 return Err(SnapshotError::MissingMetadata(db.into()));
255 };
256 let (schema, idents, interner) = codec::decode_metadata(&metadata)?;
257 let datoms = load_index_keys(store, &roots[0])
258 .await?
259 .into_iter()
260 .map(|key| Datom::from_key(IndexOrder::Eavt, &key))
261 .collect::<Result<Vec<_>, _>>()?;
262 Ok(Some(Db::from_current_snapshot(
263 root.index_basis_t,
264 schema,
265 idents,
266 interner,
267 datoms,
268 )))
269}
270
271async fn load_index_keys(store: &dyn PeerStorage, id: &BlobId) -> Result<Vec<Vec<u8>>, StoreError> {
274 let blob = store
275 .get_blob(id)
276 .await?
277 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
278 if !is_index_manifest(&blob) {
279 return decode_segment_keys(&blob);
280 }
281 let mut keys = Vec::new();
282 for child in decode_index_manifest(&blob)? {
283 let chunk = store
284 .get_blob(&child)
285 .await?
286 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
287 keys.extend(decode_segment_keys(&chunk)?);
288 }
289 Ok(keys)
290}
291
292pub struct SegmentSource<S> {
294 store: Arc<S>,
295 cache: SegmentCache,
296}
297
298impl<S: BlobStore + RootStore> SegmentSource<S> {
299 #[must_use]
301 pub fn new(store: Arc<S>) -> Self {
302 Self {
303 store,
304 cache: SegmentCache::default(),
305 }
306 }
307
308 pub async fn index_root(&self, db: &str) -> Result<Option<DbRoot>, StoreError> {
313 Ok(self
314 .store
315 .get_root(&db_root_name(db))
316 .await?
317 .as_deref()
318 .and_then(DbRoot::decode))
319 }
320
321 pub async fn lease_holder_endpoint(&self, db: &str) -> Result<Option<String>, StoreError> {
329 Ok(self
330 .index_root(db)
331 .await?
332 .and_then(|root| (!root.owner_endpoint.is_empty()).then_some(root.owner_endpoint)))
333 }
334
335 pub async fn segment(
342 &self,
343 root: &DbRoot,
344 order: IndexOrder,
345 ) -> Result<Option<Arc<[u8]>>, StoreError> {
346 let Some(roots) = &root.roots else {
347 return Ok(None);
348 };
349 let slot = match order {
350 IndexOrder::Eavt => 0,
351 IndexOrder::Aevt => 1,
352 IndexOrder::Avet => 2,
353 IndexOrder::Vaet => 3,
354 };
355 let Some(blob) = self
356 .cache
357 .get_or_load(self.store.as_ref(), &roots[slot])
358 .await?
359 else {
360 return Ok(None);
361 };
362 if !is_index_manifest(&blob) {
363 return Ok(Some(blob));
364 }
365 let mut bytes = Vec::new();
366 for child in decode_index_manifest(&blob)? {
367 let chunk = self
368 .cache
369 .get_or_load(self.store.as_ref(), &child)
370 .await?
371 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
372 bytes.extend_from_slice(&chunk);
373 }
374 Ok(Some(bytes.into()))
375 }
376
377 pub fn segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
383 decode_segment_keys(bytes)
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use corium_core::{EntityId, KeywordInterner, Value};
390 use corium_db::Idents;
391 use corium_store::{MemoryStore, RootStore};
392
393 use super::*;
394
395 #[tokio::test]
396 async fn loads_published_eavt_snapshot() {
397 let store = MemoryStore::default();
398 let datom = Datom {
399 e: EntityId::from_raw(1_001),
400 a: EntityId::from_raw(101),
401 v: Value::Str("snapshot".into()),
402 tx: EntityId::from_raw(37),
403 added: true,
404 };
405 let key = datom.key(IndexOrder::Eavt);
406 let mut segment = Vec::new();
407 segment.extend_from_slice(&(key.len() as u64).to_be_bytes());
408 segment.extend_from_slice(&key);
409 let id = store.put(&segment).await.expect("put segment");
410 let root = DbRoot {
411 format_version: FORMAT_VERSION,
412 lease_version: 1,
413 owner: "test".into(),
414 lease_expires_unix_ms: 0,
415 owner_endpoint: String::new(),
416 index_basis_t: 37,
417 roots: Some([id.clone(), id.clone(), id.clone(), id]),
418 next_entity_id: 1_005,
419 last_tx_instant: 0,
420 key_manifest_version: 0,
421 };
422 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
423 .await
424 .expect("put root");
425 let metadata = codec::encode_metadata(
426 &corium_core::Schema::default(),
427 &Idents::default(),
428 &KeywordInterner::default(),
429 );
430 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
431 .await
432 .expect("put metadata");
433
434 let db = load_current_snapshot(&store, "music")
435 .await
436 .expect("load snapshot")
437 .expect("published snapshot");
438 assert_eq!(db.basis_t(), 37);
439 assert_eq!(db.datoms(), vec![datom]);
440 }
441
442 #[tokio::test]
443 async fn loads_chunked_manifest_snapshot() {
444 let store = MemoryStore::default();
445 let datoms: Vec<Datom> = (0..4u64)
446 .map(|n| Datom {
447 e: EntityId::from_raw(1_001 + n),
448 a: EntityId::from_raw(101),
449 v: Value::Long(i64::try_from(n).unwrap()),
450 tx: EntityId::from_raw(37),
451 added: true,
452 })
453 .collect();
454 let mut chunk_ids = Vec::new();
456 for pair in datoms.chunks(2) {
457 let mut chunk = Vec::new();
458 for datom in pair {
459 let key = datom.key(IndexOrder::Eavt);
460 chunk.extend_from_slice(&(key.len() as u64).to_be_bytes());
461 chunk.extend_from_slice(&key);
462 }
463 chunk_ids.push(store.put(&chunk).await.expect("put chunk"));
464 }
465 let manifest = corium_store::encode_index_manifest(&chunk_ids);
466 let id = store.put(&manifest).await.expect("put manifest");
467 let root = DbRoot {
468 format_version: FORMAT_VERSION,
469 lease_version: 1,
470 owner: "test".into(),
471 lease_expires_unix_ms: 0,
472 owner_endpoint: String::new(),
473 index_basis_t: 37,
474 roots: Some([id.clone(), id.clone(), id.clone(), id]),
475 next_entity_id: 1_005,
476 last_tx_instant: 0,
477 key_manifest_version: 0,
478 };
479 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
480 .await
481 .expect("put root");
482 let metadata = codec::encode_metadata(
483 &corium_core::Schema::default(),
484 &Idents::default(),
485 &KeywordInterner::default(),
486 );
487 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
488 .await
489 .expect("put metadata");
490
491 let db = load_current_snapshot(&store, "music")
492 .await
493 .expect("load snapshot")
494 .expect("published snapshot");
495 assert_eq!(db.basis_t(), 37);
496 assert_eq!(db.datoms(), datoms);
497 }
498
499 #[tokio::test]
500 async fn absent_publication_falls_back_to_log_replay() {
501 assert!(
502 load_current_snapshot(&MemoryStore::default(), "music")
503 .await
504 .expect("load snapshot")
505 .is_none()
506 );
507 }
508}