1use std::collections::BTreeSet;
2
3use crate::*;
4use coven_protocol::store_commit::{
5 snapshot_image_semantic_prefix, SnapshotMeta, StoreSnapshotRef,
6};
7
8use super::*;
9
10impl StoreSession<'_> {
11 fn outbound_snapshot_publication(
12 &mut self,
13 ) -> Result<Option<DurableSnapshotPublication>, DbError> {
14 let authority = self.local_store_authority()?;
15 load_outbound_store_snapshot_on(self.conn, self.store_dir, &authority)
16 }
17
18 fn stage_snapshot_publication(
19 &mut self,
20 stage: StoreSnapshotPublicationStage,
21 meta: SnapshotMeta,
22 meta_prepared: PreparedExactObject,
23 publication: coven_protocol::prepared_commit::PreparedStorePublication,
24 rollup_bytes: Vec<u8>,
25 rollup_prepared: PreparedExactObject,
26 image: SnapshotDatabaseImage,
27 image_prepared: PreparedExactObject,
28 blobs: Vec<PreparedSnapshotBlob>,
29 ) -> Result<StoreSnapshotRef, DbError> {
30 let authority = self.local_store_authority()?;
31 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
32 let image_facts =
33 crate::payload_store::write_payload_file_blocking(&tx, self.store_dir, image.path())
34 .map_err(|source| SnapshotImageError::ProjectionPayloadStore {
35 operation: "spool Store snapshot image".to_string(),
36 source,
37 });
38 let (image_hash, _) = image.finish(image_facts).map_err(snapshot_image_db_error)?;
39 let image_prepared_hash = crate::payload_store::write_payload_blocking(
40 &tx,
41 self.store_dir,
42 image_prepared.stored_bytes(),
43 )
44 .map_err(|error| DbError::context("spool prepared Store snapshot image", error))?;
45 let image_prepared_size = image_prepared.stored_bytes().len() as u64;
46 let registration_ref = authority.reference();
47 let registration = authority.value();
48 validate_snapshot_author(&meta.author_registration, registration_ref, "Store")?;
49 validate_snapshot_image(
50 &meta.image,
51 &image_prepared,
52 image_hash,
53 image_prepared_hash,
54 image_prepared_size,
55 format!(
56 "{}.db",
57 snapshot_image_semantic_prefix(
58 meta_prepared.reference().slot(),
59 meta.image.image_hash,
60 )
61 ),
62 "Store",
63 )?;
64 let reference = StoreSnapshotRef {
65 snapshot_hash: meta.snapshot_hash(),
66 object: meta_prepared.reference().clone(),
67 };
68 SnapshotMeta::parse_at(
69 &meta.to_bytes(),
70 registration.store_root.store_root_hash,
71 &reference,
72 registration,
73 )
74 .map_err(|error| DbError::context("verify staged Store snapshot metadata", error))?;
75 publication
76 .validate_snapshot_shape(&meta, &reference)
77 .map_err(|error| DbError::context("verify staged Store snapshot publication", error))?;
78 coven_protocol::store_commit::MembershipRollup::parse_at(
79 &rollup_bytes,
80 registration.store_root.store_root_hash,
81 &meta.membership_rollup,
82 registration,
83 )
84 .map_err(|error| DbError::context("verify staged membership rollup", error))?;
85 if rollup_prepared.reference() != &meta.membership_rollup.object {
86 return Err(DbError::Message(
87 "staged membership rollup differs from the snapshot that names it".to_string(),
88 ));
89 }
90 let rollup_hash =
94 crate::payload_store::write_payload_blocking(&tx, self.store_dir, &rollup_bytes)
95 .map_err(|error| DbError::context("spool membership rollup", error))?;
96 let rollup_prepared_hash = crate::payload_store::write_payload_blocking(
97 &tx,
98 self.store_dir,
99 rollup_prepared.stored_bytes(),
100 )
101 .map_err(|error| DbError::context("spool prepared membership rollup", error))?;
102 if rollup_hash != meta.membership_rollup.rollup_hash {
103 return Err(DbError::Message(
104 "staged membership rollup bytes differ from the hash the snapshot names"
105 .to_string(),
106 ));
107 }
108 let snapshot_owner = coven_protocol::remote_object::SnapshotObjectOwner::Store {
109 metadata_slot: reference.object.slot().clone(),
110 };
111 let image_bytes =
114 crate::payload_store::read_verified_payload_blocking(&tx, self.store_dir, image_hash)
115 .map_err(|error| DbError::context("read staged Store snapshot image", error))?;
116 let mut captured = Connection::open_in_memory()?;
117 crate::connection_io::deserialize_database_image_into(&mut captured, &image_bytes)?;
118 let captured_gates = Gates::from_tables(&captured, self.synced_tables)?;
119 validate_snapshot_blob_plans_on(
120 &captured,
121 &captured_gates,
122 self.synced_tables,
123 &snapshot_owner,
124 &blobs,
125 )?;
126 drop(captured);
127 drop(image_bytes);
128 let mut active_publication = ActiveStorePublication::snapshot(publication)?;
129 match stage {
130 StoreSnapshotPublicationStage::Initial => {
131 match super::active_store_publication::claim_active_store_publication_on(
132 &tx,
133 &active_publication,
134 )? {
135 super::active_store_publication::ActiveStorePublicationClaim::Acquired => {}
136 super::active_store_publication::ActiveStorePublicationClaim::AlreadyOwned => {
137 return Err(DbError::Message(
138 "Store snapshot already owns publication before its journal"
139 .to_string(),
140 ));
141 }
142 super::active_store_publication::ActiveStorePublicationClaim::Occupied(
143 owner,
144 ) => {
145 return Err(DbError::Message(format!(
146 "another local Store operation owns publication: {owner:?}"
147 )));
148 }
149 }
150 }
151 StoreSnapshotPublicationStage::Replacing { previous, accepted } => {
152 let old = load_outbound_store_snapshot_on(&tx, self.store_dir, &authority)?
153 .ok_or_else(|| {
154 DbError::Message("replacement snapshot has no pending candidate".into())
155 })?;
156 if old.reference != previous {
157 return Err(DbError::Message(
158 "snapshot candidate changed before replacement".into(),
159 ));
160 }
161 let active =
162 super::active_store_publication::load_active_store_publication_on(&tx)?
163 .ok_or_else(|| {
164 DbError::Message("snapshot replacement has no active owner".into())
165 })?;
166 if active.owner() != &ActiveStorePublicationOwner::Snapshot
167 || active.attempt()? != &old.publication
168 || !active.retired_snapshot_objects().is_empty()
169 {
170 return Err(DbError::Message(
171 "snapshot replacement differs from its active owner".into(),
172 ));
173 }
174 let observed =
175 super::observed_store_publication::load_store_current_publication_on(&tx)?;
176 if observed.record() != accepted.interval().current()
177 || observed.observed_version() != accepted.current_version()
178 || active_publication.attempt()?.previous != *observed.record()
179 || Some(&active_publication.attempt()?.previous_version)
180 != observed.observed_version()
181 {
182 return Err(DbError::Message(
183 "snapshot replacement does not extend its installed winner".into(),
184 ));
185 }
186 let old_entry = old.publication.reference()?;
187 let winner = accepted
188 .interval()
189 .entries()
190 .iter()
191 .find(|entry| entry.reference().position == old_entry.position)
192 .ok_or_else(|| {
193 DbError::Message(
194 "snapshot replacement lacks its settled exact position".into(),
195 )
196 })?;
197 if winner.entry().previous_state_hash != old.publication.previous.state_hash() {
198 return Err(DbError::Message(
199 "snapshot winner names another exact predecessor".into(),
200 ));
201 }
202 if winner.reference() == &old_entry || accepted.interval().entries().iter().any(|entry|
203 matches!(&entry.entry().payload, coven_protocol::store_commit::StorePublicationPayload::Snapshot(snapshot) if snapshot == &old.reference))
204 {
205 return Err(DbError::Message("an accepted snapshot cannot be replaced as an unaccepted candidate".into()));
206 }
207 let retained = BTreeSet::from([
208 reference.object.clone(),
209 meta.image.object.clone(),
210 meta.membership_rollup.object.clone(),
211 active_publication.attempt()?.entry_object.clone(),
212 ]);
213 let cleanup = snapshot_candidate_cleanup_on(&tx, &old, &retained)?;
214 active_publication.retain_snapshot_cleanup(cleanup)?;
215 super::active_store_publication::update_active_store_publication_on(
216 &tx,
217 &active,
218 &active_publication,
219 )?;
220 tx.execute(
221 "DELETE FROM outbound_store_snapshot WHERE singleton = 1",
222 [],
223 )?;
224 }
225 }
226 tx.execute(
227 "INSERT INTO outbound_store_snapshot \
228 (singleton, snapshot_ref, meta_prepared, meta_bytes, blobs) \
229 VALUES (1, ?1, ?2, ?3, ?4)",
230 rusqlite::params![
231 serde_json::to_string(&reference).map_err(|error| {
232 DbError::context("serialize exact Store snapshot ref", error)
233 })?,
234 serde_json::to_string(&meta_prepared).map_err(|error| {
235 DbError::context("serialize prepared Store snapshot metadata", error)
236 })?,
237 meta.to_bytes(),
238 serde_json::to_string(&blobs).map_err(|error| {
239 DbError::context("serialize prepared Store snapshot blobs", error)
240 })?,
241 ],
242 )
243 .map_err(DbError::from)?;
244 crate::payload_store::set_payload_owner_claims_on(
245 &tx,
246 crate::payload_store::OUTBOUND_STORE_SNAPSHOT_OWNER_KEY,
247 &BTreeSet::from([
248 image_hash,
249 image_prepared_hash,
250 rollup_hash,
251 rollup_prepared_hash,
252 ]),
253 )?;
254 tx.commit().map_err(DbError::from)?;
255 Ok(reference)
256 }
257
258 fn supersede_snapshot_publication(
259 &mut self,
260 previous: StoreSnapshotRef,
261 snapshot: PublishedStoreSnapshot,
262 accepted: AcceptedStorePublicationInterval,
263 ) -> Result<(), DbError> {
264 let baseline = self.installed_replay_baseline()?;
265 if baseline.snapshot() != Some(&snapshot) {
266 return Err(DbError::Message(
267 "superseding snapshot is not the installed verified baseline".into(),
268 ));
269 }
270 let authority = self.local_store_authority()?;
271 let tx = self.conn.unchecked_transaction()?;
272 let old = load_outbound_store_snapshot_on(&tx, self.store_dir, &authority)?
273 .ok_or_else(|| DbError::Message("superseded snapshot has no pending request".into()))?;
274 let active = super::active_store_publication::load_active_store_publication_on(&tx)?
275 .ok_or_else(|| DbError::Message("superseded snapshot has no active owner".into()))?;
276 if old.reference != previous || active.attempt()? != &old.publication {
277 return Err(DbError::Message(
278 "snapshot request changed before supersession".into(),
279 ));
280 }
281 let observed = super::observed_store_publication::load_store_current_publication_on(&tx)?;
282 if observed.record() != accepted.interval().current()
283 || observed.observed_version() != accepted.current_version()
284 {
285 return Err(DbError::Message(
286 "superseding snapshot has another installed publication interval".into(),
287 ));
288 }
289 let entry = accepted.interval().entries().iter().find(|entry|
290 matches!(&entry.entry().payload, coven_protocol::store_commit::StorePublicationPayload::Snapshot(reference) if reference == &snapshot.reference)
291 ).ok_or_else(|| DbError::Message("superseding snapshot is absent from the accepted interval".into()))?;
292 if entry.reference().position <= old.publication.reference()?.position
293 || entry.entry().previous_state_hash
294 != snapshot.meta.publication_predecessor.state_hash()
295 || !snapshot.meta.coverage.covers(&old.meta.value.coverage)
296 {
297 return Err(DbError::Message(
298 "accepted snapshot does not supersede the requested checkpoint".into(),
299 ));
300 }
301 let retained = BTreeSet::from([
302 snapshot.reference.object.clone(),
303 snapshot.meta.image.object.clone(),
304 snapshot.meta.membership_rollup.object.clone(),
305 entry.reference().object.clone(),
306 ]);
307 let cleanup = snapshot_candidate_cleanup_on(&tx, &old, &retained)?;
308 let superseded = active.supersede_snapshot(snapshot, cleanup)?;
309 super::active_store_publication::update_active_store_publication_on(
310 &tx,
311 &active,
312 &superseded,
313 )?;
314 tx.commit()?;
315 Ok(())
316 }
317
318 fn complete_superseded_snapshot_publication(
319 &mut self,
320 expected: ActiveStorePublication,
321 ) -> Result<SnapshotMeta, DbError> {
322 let snapshot = expected.superseding_snapshot().ok_or_else(|| {
323 DbError::Message("snapshot request has no verified superseding checkpoint".into())
324 })?;
325 let authority = self.local_store_authority()?;
326 let tx = self.conn.unchecked_transaction()?;
327 let pending = load_outbound_store_snapshot_on(&tx, self.store_dir, &authority)?
328 .ok_or_else(|| DbError::Message("superseded snapshot request is absent".into()))?;
329 if expected.attempt()? != &pending.publication {
330 return Err(DbError::Message(
331 "superseded snapshot differs from its original request".into(),
332 ));
333 }
334 super::active_store_publication::clear_active_store_publication_on(&tx, &expected)?;
335 tx.execute(
336 "DELETE FROM outbound_store_snapshot WHERE singleton = 1",
337 [],
338 )?;
339 crate::payload_store::release_payload_owner_on(
340 &tx,
341 crate::payload_store::OUTBOUND_STORE_SNAPSHOT_OWNER_KEY,
342 )?;
343 tx.commit()?;
344 Ok(snapshot.meta.clone())
345 }
346
347 fn complete_snapshot_candidate_cleanup(
348 &self,
349 expected: ActiveStorePublication,
350 ) -> Result<(), DbError> {
351 let mut replacement = expected.clone();
352 replacement.complete_snapshot_cleanup()?;
353 super::active_store_publication::update_active_store_publication_on(
354 self.conn,
355 &expected,
356 &replacement,
357 )
358 }
359
360 fn latest_local_store_snapshot(&mut self) -> Result<Option<PublishedStoreSnapshot>, DbError> {
361 let root = self.required_root_authority()?;
362 StoreRecords::new(self.conn, self.store_dir)
363 .published_store_snapshot(&root, self.verified_store_authority)
364 }
365
366 #[cfg(any(test, feature = "test-utils"))]
367 fn local_store_snapshots(&mut self) -> Result<Vec<PublishedStoreSnapshot>, DbError> {
368 let root = self.required_root_authority()?;
369 StoreRecords::new(self.conn, self.store_dir)
370 .published_store_snapshots(&root, self.verified_store_authority)
371 }
372
373 fn complete_snapshot_publication(
374 &mut self,
375 accepted: crate::AcceptedStorePublicationInterval,
376 ) -> Result<SnapshotMeta, DbError> {
377 let authority = self.local_store_authority()?;
378 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
379 let outbound = load_outbound_store_snapshot_on(&tx, self.store_dir, &authority)?
380 .ok_or_else(|| DbError::Message("outbound Store snapshot is absent".to_string()))?;
381 let accepted_entry = accepted
382 .interval()
383 .entries()
384 .iter()
385 .find(|entry| {
386 matches!(
387 &entry.entry().payload,
388 coven_protocol::store_commit::StorePublicationPayload::Snapshot(reference)
389 if reference == &outbound.reference
390 )
391 })
392 .ok_or_else(|| {
393 DbError::Message(
394 "accepted Store publication does not contain the prepared snapshot".to_string(),
395 )
396 })?;
397 if accepted.interval().previous() != &*outbound.publication.previous
398 || accepted_entry.entry() != &outbound.publication.entry
399 || accepted_entry.reference().object != outbound.publication.entry_object
400 {
401 return Err(DbError::Message(
402 "accepted Store snapshot differs from the prepared publication".to_string(),
403 ));
404 }
405 let superseded = StoreTransaction::new(&tx, self.store_dir)
406 .retire_snapshot_artifact_ownership(
407 self.verified_store_authority,
408 &authority.value().store_root,
409 &coven_protocol::store_commit::AcceptedStoreSnapshotRef {
410 snapshot: outbound.reference.clone(),
411 publication: accepted_entry.reference().clone(),
412 },
413 &outbound.meta.value,
414 )?;
415 install_snapshot_blob_plans_on(&tx, &outbound.blobs)?;
416 let snapshot_owner = coven_protocol::remote_object::SnapshotObjectOwner::Store {
417 metadata_slot: outbound.reference.object.slot().clone(),
418 };
419 persist_snapshot_image_on(
420 &tx,
421 self.store_dir,
422 &outbound.meta.value.image,
423 snapshot_owner.clone(),
424 "Store snapshot image",
425 )?;
426 crate::snapshot_objects::persist_membership_rollup_on(
427 &tx,
428 self.store_dir,
429 &outbound.meta.value.membership_rollup,
430 snapshot_owner,
431 "Store membership rollup",
432 )?;
433 StoreTransaction::new(&tx, self.store_dir)
434 .retire_store_blob_snapshot_ownership(outbound.reference.object.slot(), &superseded)?;
435 let deleted = tx
436 .execute(
437 "DELETE FROM outbound_store_snapshot \
438 WHERE singleton = 1 AND snapshot_ref = ?1",
439 [serde_json::to_string(&outbound.reference).map_err(|error| {
440 DbError::context("serialize accepted Store snapshot ref", error)
441 })?],
442 )
443 .map_err(DbError::from)?;
444 if deleted != 1 {
445 return Err(DbError::Message(
446 "outbound snapshot ownership row is absent or changed".to_string(),
447 ));
448 }
449 crate::payload_store::release_payload_owner_on(
450 &tx,
451 crate::payload_store::OUTBOUND_STORE_SNAPSHOT_OWNER_KEY,
452 )?;
453 let accepted_position =
454 i64::try_from(accepted_entry.reference().position.get()).map_err(|_| {
455 DbError::Message("Store snapshot position exceeds SQLite integer".into())
456 })?;
457 tx.execute(
458 "INSERT INTO published_store_snapshot \
459 (publication_position, snapshot_ref, meta_bytes) VALUES (?1, ?2, ?3)",
460 rusqlite::params![
461 accepted_position,
462 serde_json::to_string(&outbound.reference).map_err(|error| {
463 DbError::context("serialize published Store snapshot ref", error)
464 })?,
465 outbound.meta.bytes,
466 ],
467 )
468 .map_err(DbError::from)?;
469 let expected = super::observed_store_publication::load_store_current_publication_on(&tx)?;
470 if expected.record() != accepted.interval().current() {
471 super::observed_store_publication::install_store_publication_interval_on(
472 &tx, &expected, &accepted,
473 )?;
474 } else if expected.observed_version() != accepted.current_version() {
475 return Err(DbError::Message(
476 "accepted Store snapshot revision differs from the installed boundary".into(),
477 ));
478 }
479 let active_publication = ActiveStorePublication::snapshot(outbound.publication.clone())?;
480 super::active_store_publication::clear_active_store_publication_on(
481 &tx,
482 &active_publication,
483 )?;
484 tx.commit().map_err(DbError::from)?;
485 Ok(outbound.meta.value)
486 }
487}
488
489impl StoreDatabase {
490 pub async fn outbound_snapshot_publication(
491 &self,
492 ) -> Result<Option<DurableSnapshotPublication>, DbError> {
493 self.call_store(|session| session.outbound_snapshot_publication())
494 .await
495 }
496
497 #[allow(clippy::too_many_arguments)]
498 pub async fn stage_snapshot_publication(
499 &self,
500 stage: StoreSnapshotPublicationStage,
501 meta: SnapshotMeta,
502 meta_prepared: PreparedExactObject,
503 publication: coven_protocol::prepared_commit::PreparedStorePublication,
504 rollup_bytes: Vec<u8>,
505 rollup_prepared: PreparedExactObject,
506 image: SnapshotDatabaseImage,
507 image_prepared: PreparedExactObject,
508 blobs: Vec<PreparedSnapshotBlob>,
509 ) -> Result<StoreSnapshotRef, DbError> {
510 self.call_store(move |session| {
511 session.stage_snapshot_publication(
512 stage,
513 meta,
514 meta_prepared,
515 publication,
516 rollup_bytes,
517 rollup_prepared,
518 image,
519 image_prepared,
520 blobs,
521 )
522 })
523 .await
524 }
525
526 pub async fn supersede_snapshot_publication(
527 &self,
528 previous: StoreSnapshotRef,
529 snapshot: PublishedStoreSnapshot,
530 accepted: AcceptedStorePublicationInterval,
531 ) -> Result<(), DbError> {
532 self.call_store(move |session| {
533 session.supersede_snapshot_publication(previous, snapshot, accepted)
534 })
535 .await
536 }
537
538 pub async fn complete_superseded_snapshot_publication(
539 &self,
540 expected: ActiveStorePublication,
541 ) -> Result<SnapshotMeta, DbError> {
542 self.call_store(move |session| session.complete_superseded_snapshot_publication(expected))
543 .await
544 }
545
546 pub async fn complete_snapshot_candidate_cleanup(
547 &self,
548 expected: ActiveStorePublication,
549 ) -> Result<(), DbError> {
550 self.call_store(move |session| session.complete_snapshot_candidate_cleanup(expected))
551 .await
552 }
553
554 pub async fn latest_local_store_snapshot(
555 &self,
556 ) -> Result<Option<PublishedStoreSnapshot>, DbError> {
557 self.call_store(|session| session.latest_local_store_snapshot())
558 .await
559 }
560
561 #[cfg(any(test, feature = "test-utils"))]
562 pub async fn local_store_snapshots(&self) -> Result<Vec<PublishedStoreSnapshot>, DbError> {
563 self.call_store(|session| session.local_store_snapshots())
564 .await
565 }
566
567 pub async fn complete_snapshot_publication(
568 &self,
569 accepted: crate::AcceptedStorePublicationInterval,
570 ) -> Result<SnapshotMeta, DbError> {
571 self.call_store(move |session| session.complete_snapshot_publication(accepted))
572 .await
573 }
574}
575
576fn snapshot_candidate_cleanup_on(
580 connection: &Connection,
581 old: &DurableSnapshotPublication,
582 retained: &BTreeSet<ExactObjectRef>,
583) -> Result<Vec<ExactObjectRef>, DbError> {
584 let mut cleanup = Vec::new();
585 for object in BTreeSet::from([
586 old.reference.object.clone(),
587 old.meta.value.image.object.clone(),
588 old.meta.value.membership_rollup.object.clone(),
589 old.publication.entry_object.clone(),
590 ]) {
591 if retained.contains(&object) {
592 continue;
593 }
594 let object_id = coven_protocol::remote_object::remote_object_id(&object);
595 let owned: bool = connection.query_row(
596 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
597 [object_id.to_string()],
598 |row| row.get(0),
599 )?;
600 if owned {
601 crate::load_remote_object_on(connection, object_id)?;
602 } else {
603 cleanup.push(object);
604 }
605 }
606 Ok(cleanup)
607}