1mod retained_materialization;
2
3use crate::DbError;
4use coven_protocol::objects::ExactObjectVersion;
5use coven_protocol::store_commit::StoreCurrentPublicationRecord;
6use rusqlite::OptionalExtension;
7
8use super::StoreSession;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ObservedStorePublication {
12 record: StoreCurrentPublicationRecord,
13 version: ExactObjectVersion,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum StorePublicationBoundary {
20 AcceptedPrefix(StoreCurrentPublicationRecord),
21 Observed(ObservedStorePublication),
22}
23
24impl StorePublicationBoundary {
25 pub fn record(&self) -> &StoreCurrentPublicationRecord {
26 match self {
27 Self::AcceptedPrefix(record) => record,
28 Self::Observed(observed) => observed.record(),
29 }
30 }
31
32 pub fn observed_version(&self) -> Option<&ExactObjectVersion> {
33 match self {
34 Self::AcceptedPrefix(_) => None,
35 Self::Observed(observed) => Some(observed.version()),
36 }
37 }
38
39 pub fn require_observed(&self) -> Result<&ObservedStorePublication, DbError> {
40 match self {
41 Self::Observed(observed) => Ok(observed),
42 Self::AcceptedPrefix(_) => Err(DbError::Message(
43 "conditional Store publication requires a current provider observation".into(),
44 )),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct AcceptedStoreCommitPublication {
51 publication: coven_protocol::store_commit::StoreCommitPublication,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct AcceptedStoreCommitEvidence {
56 acceptance: StoreCommitAcceptance,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60enum StoreCommitAcceptance {
61 Exact(AcceptedStoreCommitPublication),
62 SnapshotCovered {
63 commit: coven_protocol::store_commit::StoreBatchCommitRef,
64 snapshot: coven_protocol::store_commit::StoreSnapshotRef,
65 },
66}
67
68impl AcceptedStoreCommitEvidence {
69 pub(crate) fn from_snapshot(
70 authority: &coven_protocol::store_commit::RetainedReplaySnapshotAuthority,
71 commit: &coven_protocol::store_commit::StoreBatchCommitRef,
72 ) -> Result<Self, DbError> {
73 authority.validate()?;
74 if !authority.metadata.coverage.covers_commit(commit)
75 || authority
76 .metadata
77 .history_summary
78 .causal_cut
79 .get(&commit.coord)
80 != Some(commit)
81 {
82 return Err(DbError::Message(
83 "retained Store commit is absent from its exact snapshot history".to_string(),
84 ));
85 }
86 Ok(Self {
87 acceptance: StoreCommitAcceptance::SnapshotCovered {
88 commit: commit.clone(),
89 snapshot: authority.snapshot.clone(),
90 },
91 })
92 }
93
94 pub fn commit_ref(&self) -> &coven_protocol::store_commit::StoreBatchCommitRef {
95 match &self.acceptance {
96 StoreCommitAcceptance::Exact(publication) => match &publication.entry().payload {
97 coven_protocol::store_commit::StorePublicationPayload::Commit(commit) => commit,
98 coven_protocol::store_commit::StorePublicationPayload::Snapshot(_) => {
99 unreachable!("verified commit publication contains a commit")
100 }
101 },
102 StoreCommitAcceptance::SnapshotCovered { commit, .. } => commit,
103 }
104 }
105
106 pub fn exact_publication(&self) -> Option<&AcceptedStoreCommitPublication> {
107 match &self.acceptance {
108 StoreCommitAcceptance::Exact(publication) => Some(publication),
109 StoreCommitAcceptance::SnapshotCovered { .. } => None,
110 }
111 }
112}
113
114impl From<AcceptedStoreCommitPublication> for AcceptedStoreCommitEvidence {
115 fn from(publication: AcceptedStoreCommitPublication) -> Self {
116 Self {
117 acceptance: StoreCommitAcceptance::Exact(publication),
118 }
119 }
120}
121
122impl AcceptedStoreCommitPublication {
123 pub fn from_verified(
124 accepted: coven_protocol::store_commit::AcceptedStoreCommitPublication,
125 ) -> Self {
126 Self {
127 publication: accepted.into_publication(),
128 }
129 }
130
131 pub fn entry(&self) -> &coven_protocol::store_commit::StorePublicationEntry {
132 self.publication.entry()
133 }
134
135 pub fn reference(&self) -> &coven_protocol::store_commit::StorePublicationRef {
136 self.publication.reference()
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct AcceptedStorePublicationInterval {
142 interval: coven_protocol::store_commit::VerifiedStorePublicationInterval,
143 current_version: Option<ExactObjectVersion>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum StoreCommitPublicationOutcome {
149 Accepted {
150 interval: AcceptedStorePublicationInterval,
151 accepted_predecessor: coven_protocol::membership::MembershipFloor,
152 },
153 Installed(AcceptedStoreCommitEvidence),
154}
155
156impl StoreCommitPublicationOutcome {
157 pub(super) fn resolve_installed_on(
161 self,
162 store: super::StoreTransaction<'_, '_>,
163 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
164 ) -> Result<Self, DbError> {
165 let expected = match &self {
166 Self::Accepted { interval, .. } => interval.accepted_commit(commit)?.into(),
167 Self::Installed(evidence) => {
168 if evidence.commit_ref() != commit.reference() {
169 return Err(DbError::Message(
170 "Store publication receipt belongs to another commit".to_string(),
171 ));
172 }
173 evidence.clone()
174 }
175 };
176 match installed_store_commit_evidence_on(store, commit)? {
177 Some(installed) => {
178 if let (Some(current), Some(expected)) =
179 (installed.exact_publication(), expected.exact_publication())
180 {
181 if current != expected {
182 return Err(DbError::Message(
183 "installed Store commit has a different accepted publication"
184 .to_string(),
185 ));
186 }
187 }
188 Ok(Self::Installed(installed))
189 }
190 None => match self {
191 Self::Accepted { .. } => Ok(self),
192 Self::Installed(_) => Err(DbError::Message(
193 "Store publication receipt has no installed commit authority".to_string(),
194 )),
195 },
196 }
197 }
198
199 pub(super) fn requires_materialization(&self) -> bool {
200 matches!(self, Self::Accepted { .. })
201 }
202
203 pub(super) fn install_on(
204 &self,
205 store: super::StoreTransaction<'_, '_>,
206 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
207 ) -> Result<AcceptedStoreCommitEvidence, DbError> {
208 match self {
209 Self::Accepted { interval, .. } => {
210 install_accepted_store_commit_interval_on(store.transaction, interval, commit)
211 .map(Into::into)
212 }
213 Self::Installed(evidence) => {
214 let installed = installed_store_commit_evidence_on(store, commit)?;
215 if installed.as_ref() != Some(evidence) {
216 return Err(DbError::Message(
217 "Store publication receipt differs from installed commit authority"
218 .to_string(),
219 ));
220 }
221 Ok(evidence.clone())
222 }
223 }
224 }
225}
226
227fn installed_store_commit_evidence_on(
228 store: super::StoreTransaction<'_, '_>,
229 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
230) -> Result<Option<AcceptedStoreCommitEvidence>, DbError> {
231 let connection = store.transaction;
232 let current = load_store_current_publication_on(connection)?;
233 if current.record().store_root_hash != commit.store_root_hash() {
234 return Err(DbError::Message(
235 "Store commit belongs to another installed Store".to_string(),
236 ));
237 }
238 let reference = commit.reference();
239 let stream_id = reference.coord.stream_id.to_string();
240 let coverage = super::materialized_commit_index::snapshot_coverage_on(connection)?;
241 if coverage
242 .get(&stream_id)
243 .is_some_and(|tip| reference.coord.sequence() <= tip.coord.sequence())
244 {
245 let baseline = super::retained_replay::load_replay_baseline_metadata_on(
246 super::StoreRecords::new(connection, store.store_dir),
247 )?
248 .ok_or_else(|| {
249 DbError::Message("covered Store commit has no installed replay baseline".to_string())
250 })?;
251 let crate::RetainedReplayAuthority::InstalledSnapshot(authority) = baseline.authority
252 else {
253 return Err(DbError::Message(
254 "covered Store commit has a genesis replay baseline".to_string(),
255 ));
256 };
257 return AcceptedStoreCommitEvidence::from_snapshot(&authority, reference).map(Some);
258 }
259 let installed = super::materialized_commit_index::materialized_commit_ref_on(
260 connection,
261 &stream_id,
262 reference.coord.sequence(),
263 )?;
264 match installed {
265 Some(installed) if installed == *reference => load_accepted_store_commit_on(
266 connection,
267 commit,
268 &commit.author().device_signing_pubkey,
269 )
270 .map(|accepted| Some(accepted.into())),
271 Some(_) => Err(DbError::Message(
272 "Store commit coordinate is installed with another exact commit".to_string(),
273 )),
274 None => Ok(None),
275 }
276}
277
278impl AcceptedStorePublicationInterval {
279 pub fn from_verified(
280 interval: coven_protocol::store_commit::VerifiedStorePublicationInterval,
281 current_version: Option<ExactObjectVersion>,
282 ) -> Self {
283 Self {
284 interval,
285 current_version,
286 }
287 }
288
289 pub fn interval(&self) -> &coven_protocol::store_commit::VerifiedStorePublicationInterval {
290 &self.interval
291 }
292
293 pub fn current_version(&self) -> Option<&ExactObjectVersion> {
294 self.current_version.as_ref()
295 }
296
297 pub fn accepted_commit(
298 &self,
299 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
300 ) -> Result<AcceptedStoreCommitPublication, coven_protocol::store_commit::StoreProtocolError>
301 {
302 self.interval
303 .accepted_commit(commit)
304 .map(AcceptedStoreCommitPublication::from_verified)
305 }
306}
307
308impl ObservedStorePublication {
309 pub fn from_parts(record: StoreCurrentPublicationRecord, version: ExactObjectVersion) -> Self {
310 Self { record, version }
311 }
312
313 pub fn verified_genesis(
314 record: StoreCurrentPublicationRecord,
315 version: ExactObjectVersion,
316 expected_store_root_hash: coven_protocol::store_commit::ObjectHash,
317 founder_pubkey: &str,
318 ) -> Result<Self, coven_protocol::store_commit::StoreProtocolError> {
319 record.verify_genesis(expected_store_root_hash, founder_pubkey)?;
320 Ok(Self { record, version })
321 }
322
323 pub fn record(&self) -> &StoreCurrentPublicationRecord {
324 &self.record
325 }
326
327 pub fn version(&self) -> &ExactObjectVersion {
328 &self.version
329 }
330
331 pub fn verified_commit_successor(
332 previous: &Self,
333 record: StoreCurrentPublicationRecord,
334 version: ExactObjectVersion,
335 entry: &coven_protocol::store_commit::StorePublicationEntry,
336 reference: &coven_protocol::store_commit::StorePublicationRef,
337 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
338 publisher_signing_pubkey: &str,
339 ) -> Result<Self, coven_protocol::store_commit::StoreProtocolError> {
340 record.verify_commit_transition(
341 &previous.record,
342 entry,
343 reference,
344 commit,
345 publisher_signing_pubkey,
346 )?;
347 Ok(Self { record, version })
348 }
349}
350
351pub(super) fn load_store_current_publication_on(
352 connection: &rusqlite::Connection,
353) -> Result<StorePublicationBoundary, DbError> {
354 load_store_publication_boundary_on(connection)?
355 .ok_or_else(|| DbError::Message("Store publication current record is absent".to_string()))
356}
357
358fn load_store_publication_boundary_on(
359 connection: &rusqlite::Connection,
360) -> Result<Option<StorePublicationBoundary>, DbError> {
361 connection
362 .query_row(
363 "SELECT record_hash, record_bytes, provider_version
364 FROM store_publication_current WHERE singleton = 1",
365 [],
366 |row| {
367 Ok((
368 row.get::<_, String>(0)?,
369 row.get::<_, Vec<u8>>(1)?,
370 row.get::<_, Option<String>>(2)?,
371 ))
372 },
373 )
374 .optional()
375 .map_err(DbError::from)?
376 .map(|(hash, bytes, version)| {
377 let record: StoreCurrentPublicationRecord = serde_json::from_slice(&bytes)
378 .map_err(|error| DbError::context("Store current publication record", error))?;
379 if record.to_bytes() != bytes || record.record_hash().to_string() != hash {
380 return Err(DbError::Message(
381 "Store current publication row differs from its canonical record".to_string(),
382 ));
383 }
384 match version {
385 Some(version) => Ok(StorePublicationBoundary::Observed(
386 ObservedStorePublication {
387 record,
388 version: ExactObjectVersion::from_provider(version)
389 .map_err(DbError::from)?,
390 },
391 )),
392 None => Ok(StorePublicationBoundary::AcceptedPrefix(record)),
393 }
394 })
395 .transpose()
396}
397
398fn require_unobserved_genesis_publication(
399 connection: &rusqlite::Connection,
400 store_dir: &coven_foundation::store_dir::StoreDir,
401) -> Result<(), DbError> {
402 let baseline = super::retained_replay::load_replay_baseline_on(super::StoreRecords::new(
403 connection, store_dir,
404 ))?
405 .ok_or_else(|| DbError::Message("unobserved Store has no replay baseline".to_string()))?;
406 let crate::RetainedReplayAuthority::Genesis(_) = &baseline.authority else {
407 return Err(DbError::Message(
408 "installed Store snapshot has no publication boundary".to_string(),
409 ));
410 };
411 let has_accepted_history: bool = connection
412 .query_row(
413 "SELECT EXISTS(SELECT 1 FROM materialized_commits)
414 OR EXISTS(SELECT 1 FROM retained_merge_materializations)
415 OR EXISTS(SELECT 1 FROM snapshot_coverage)
416 OR EXISTS(SELECT 1 FROM store_publication_entries)",
417 [],
418 |row| row.get(0),
419 )
420 .map_err(DbError::from)?;
421 if has_accepted_history {
422 return Err(DbError::Message(
423 "accepted Store history has no publication boundary".to_string(),
424 ));
425 }
426 Ok(())
427}
428
429pub(super) fn observe_store_publication_interval_on(
430 store: super::StoreTransaction<'_, '_>,
431 accepted: &AcceptedStorePublicationInterval,
432) -> Result<(), DbError> {
433 let expected = load_store_publication_boundary_on(store.transaction)?;
434 match &expected {
435 Some(expected) => {
436 if accepted.interval().previous() != &**expected.record()
437 && accepted.interval().current() != expected.record()
438 {
439 return Err(DbError::StorePublicationChanged);
443 }
444 install_store_publication_interval_on(store.transaction, expected, accepted)
445 }
446 None => {
447 require_unobserved_genesis_publication(store.transaction, store.store_dir)?;
448 let (root, protocol) = crate::load_store_root_authority_on(store.transaction)?
449 .ok_or(DbError::StoreRootHashMissing)?;
450 if accepted.interval().previous()
451 != &coven_protocol::store_commit::StoreCurrentPublicationRecordBody::genesis(
452 root.store_root_hash,
453 )
454 {
455 return Err(DbError::Message(
456 "initial Store publication interval does not start at its rooted genesis"
457 .to_string(),
458 ));
459 }
460 if accepted.interval().entries().is_empty() {
461 accepted
462 .interval()
463 .current()
464 .verify_genesis(root.store_root_hash, &protocol.descriptor.founder_pubkey)?;
465 }
466 persist_store_publication_interval_on(store.transaction, None, accepted)
467 }
468 }
469}
470
471pub(super) fn install_genesis_store_publication_on(
472 transaction: &rusqlite::Transaction<'_>,
473 record: &StoreCurrentPublicationRecord,
474 version: &ExactObjectVersion,
475) -> Result<(), DbError> {
476 if record.accepted().is_some() {
477 return Err(DbError::Message(
478 "initial Store publication record is not genesis".to_string(),
479 ));
480 }
481 let bytes = record.to_bytes();
482 let inserted = transaction
483 .execute(
484 "INSERT INTO store_publication_current
485 (singleton, record_hash, record_bytes, provider_version)
486 VALUES (1, ?1, ?2, ?3)
487 ON CONFLICT(singleton) DO NOTHING",
488 rusqlite::params![
489 record.record_hash().to_string(),
490 bytes,
491 version.as_provider()
492 ],
493 )
494 .map_err(DbError::from)?;
495 if inserted == 0 {
496 let current = load_store_current_publication_on(transaction)?;
497 if current.record() != record || current.observed_version() != Some(version) {
498 return Err(DbError::Message(
499 "Store publication genesis differs from installed current record".to_string(),
500 ));
501 }
502 }
503 Ok(())
504}
505
506pub(super) fn install_store_publication_interval_on(
507 transaction: &rusqlite::Transaction<'_>,
508 expected: &StorePublicationBoundary,
509 accepted: &AcceptedStorePublicationInterval,
510) -> Result<(), DbError> {
511 if accepted.interval().current() == expected.record() {
512 if load_store_current_publication_on(transaction)? != *expected {
516 return Err(DbError::Message(
517 "Store publication boundary changed before local completion".to_string(),
518 ));
519 }
520 load_store_publication_entries_on(transaction)?;
521 if expected.observed_version().is_none() {
522 if let Some(version) = accepted.current_version() {
523 let changed = transaction.execute(
524 "UPDATE store_publication_current SET provider_version = ?1
525 WHERE singleton = 1 AND record_hash = ?2 AND record_bytes = ?3
526 AND provider_version IS NULL",
527 rusqlite::params![
528 version.as_provider(),
529 expected.record().record_hash().to_string(),
530 expected.record().to_bytes()
531 ],
532 )?;
533 if changed != 1 {
534 return Err(DbError::StorePublicationChanged);
535 }
536 }
537 }
538 return Ok(());
539 }
540 if accepted.interval().previous() != &**expected.record() {
541 return Err(DbError::Message(
542 "Store publication interval starts from another local boundary".to_string(),
543 ));
544 }
545 if accepted.interval().entries().is_empty()
546 && accepted.interval().current() != expected.record()
547 {
548 return Err(DbError::Message(
549 "empty Store publication interval changes its authenticated current record".to_string(),
550 ));
551 }
552 persist_store_publication_interval_on(transaction, Some(expected), accepted)
553}
554
555fn persist_store_publication_interval_on(
556 transaction: &rusqlite::Transaction<'_>,
557 expected: Option<&StorePublicationBoundary>,
558 accepted: &AcceptedStorePublicationInterval,
559) -> Result<(), DbError> {
560 let retained = load_store_publication_entry_objects_on(transaction)?;
561 let mut coordinates = std::collections::BTreeMap::new();
562 for entry in &retained {
563 if let coven_protocol::store_commit::StorePublicationPayload::Commit(commit) =
564 &entry.value.payload
565 {
566 if coordinates
567 .insert(commit.coord.clone(), entry.prepared.reference())
568 .is_some()
569 {
570 return Err(DbError::Message(
571 "retained Store publications repeat an author sequence".to_string(),
572 ));
573 }
574 }
575 }
576 let coverage = super::materialized_commit_index::snapshot_coverage_on(transaction)?;
577 for accepted_entry in accepted.interval().entries() {
578 let reference = accepted_entry.reference();
579 if let coven_protocol::store_commit::StorePublicationPayload::Commit(commit) =
580 &accepted_entry.entry().payload
581 {
582 match coordinates.get(&commit.coord) {
583 Some(existing) if **existing == reference.object => {}
584 Some(_) => {
585 return Err(DbError::Message(
586 "Store publication reuses an already accepted author sequence".to_string(),
587 ));
588 }
589 None => {
590 if coverage
591 .get(&commit.coord.stream_id.to_string())
592 .is_some_and(|tip| commit.coord.sequence() <= tip.coord.sequence())
593 {
594 return Err(DbError::Message(
595 "Store publication reuses a snapshot-covered author sequence"
596 .to_string(),
597 ));
598 }
599 }
600 }
601 }
602 let position = i64::try_from(reference.position.get()).map_err(|_| {
603 DbError::Message("Store publication position exceeds SQLite integer".to_string())
604 })?;
605 let encoded_reference = serde_json::to_string(reference)
606 .map_err(|error| DbError::context("Store publication reference", error))?;
607 let entry_bytes = accepted_entry.entry().to_bytes();
608 let inserted = transaction
609 .execute(
610 "INSERT INTO store_publication_entries (position, entry_ref, entry_bytes)
611 VALUES (?1, ?2, ?3)
612 ON CONFLICT(position) DO NOTHING",
613 rusqlite::params![position, encoded_reference, entry_bytes],
614 )
615 .map_err(DbError::from)?;
616 if inserted == 0 {
617 let existing: (String, Vec<u8>) = transaction
618 .query_row(
619 "SELECT entry_ref, entry_bytes FROM store_publication_entries
620 WHERE position = ?1",
621 [position],
622 |row| Ok((row.get(0)?, row.get(1)?)),
623 )
624 .map_err(DbError::from)?;
625 if existing != (encoded_reference, entry_bytes) {
626 return Err(DbError::Message(
627 "Store publication position already retains another entry".to_string(),
628 ));
629 }
630 }
631 }
632 let current = accepted.interval().current();
633 let current_bytes = current.to_bytes();
634 let updated = match expected {
635 Some(expected) => transaction
636 .execute(
637 "UPDATE store_publication_current
638 SET record_hash = ?1, record_bytes = ?2, provider_version = ?3
639 WHERE singleton = 1 AND record_hash = ?4 AND record_bytes = ?5
640 AND provider_version IS ?6",
641 rusqlite::params![
642 current.record_hash().to_string(),
643 current_bytes,
644 accepted
645 .current_version()
646 .map(ExactObjectVersion::as_provider),
647 expected.record().record_hash().to_string(),
648 expected.record().to_bytes(),
649 expected
650 .observed_version()
651 .map(ExactObjectVersion::as_provider),
652 ],
653 )
654 .map_err(DbError::from)?,
655 None => transaction
656 .execute(
657 "INSERT INTO store_publication_current
658 (singleton, record_hash, record_bytes, provider_version)
659 VALUES (1, ?1, ?2, ?3)",
660 rusqlite::params![
661 current.record_hash().to_string(),
662 current_bytes,
663 accepted
664 .current_version()
665 .map(ExactObjectVersion::as_provider),
666 ],
667 )
668 .map_err(DbError::from)?,
669 };
670 if updated != 1 {
671 return Err(DbError::Message(
672 "Store publication boundary changed before local completion".to_string(),
673 ));
674 }
675 load_store_publication_entries_on(transaction)?;
676 Ok(())
677}
678
679pub(super) fn install_store_checkpoint_publication_on(
683 transaction: &rusqlite::Transaction<'_>,
684 expected: &StorePublicationBoundary,
685 accepted: &AcceptedStorePublicationInterval,
686 checkpoint: &coven_protocol::store_commit::RetainedReplaySnapshotAuthority,
687) -> Result<(), DbError> {
688 use coven_protocol::store_commit::{StorePublicationPayload, StorePublicationRef};
689
690 if load_store_current_publication_on(transaction)? != *expected {
691 return Err(DbError::Message(
692 "Store publication changed while the checkpoint image was being prepared".into(),
693 ));
694 }
695 let interval = accepted.interval();
696 let first = interval.entries().first().ok_or_else(|| {
697 DbError::Message("checkpoint installation has no accepted snapshot entry".into())
698 })?;
699 if checkpoint.store_root.store_root_hash != expected.record().store_root_hash
700 || interval.previous() != &*checkpoint.metadata.publication_predecessor
701 || first.entry().payload != StorePublicationPayload::Snapshot(checkpoint.snapshot.clone())
702 || interval.current().latest_snapshot().is_none_or(|snapshot| {
703 snapshot.snapshot != checkpoint.snapshot || &snapshot.publication != first.reference()
704 })
705 {
706 return Err(DbError::Message(
707 "checkpoint image and accepted publication name different Store boundaries".into(),
708 ));
709 }
710 if let Some(previous) = expected.record().accepted() {
711 let current = interval.current().accepted().ok_or_else(|| {
712 DbError::Message("an accepted checkpoint cannot return the Store to genesis".into())
713 })?;
714 if current.position < previous.position
715 || (current.position == previous.position && interval.current() != expected.record())
716 {
717 return Err(DbError::Message(
718 "checkpoint publication regresses or conflicts with the observed Store boundary"
719 .into(),
720 ));
721 }
722 }
723 for local in load_store_publication_entries_on(transaction)? {
724 if local.value.position < first.reference().position {
725 continue;
726 }
727 let reference =
728 StorePublicationRef::from_entry(&local.value, local.prepared.reference().clone())?;
729 if !interval
730 .entries()
731 .iter()
732 .any(|entry| entry.reference() == &reference && entry.entry() == &local.value)
733 {
734 return Err(DbError::Message(
735 "checkpoint publication conflicts with an overlapping accepted Store entry".into(),
736 ));
737 }
738 }
739 transaction.execute("DELETE FROM store_publication_entries", [])?;
742 let accepted =
743 if interval.current() == expected.record() && expected.observed_version().is_some() {
744 AcceptedStorePublicationInterval {
745 interval: interval.clone(),
746 current_version: expected.observed_version().cloned(),
747 }
748 } else {
749 accepted.clone()
750 };
751 persist_store_publication_interval_on(transaction, Some(expected), &accepted)
752}
753
754pub(super) fn retire_store_publication_prefix_before_snapshot_on(
755 store: super::StoreTransaction<'_, '_>,
756 lookup: &mut dyn super::verified_store_authority::VerifiedRegistrationLookup,
757 snapshot: &coven_protocol::store_commit::StorePublicationRef,
758) -> Result<(), DbError> {
759 let transaction = store.transaction;
760 let baseline = super::retained_replay::load_replay_baseline_metadata_on(
761 super::StoreRecords::new(transaction, store.store_dir),
762 )?
763 .ok_or_else(|| {
764 DbError::Message(
765 "Store publication retirement has no installed replay baseline".to_string(),
766 )
767 })?;
768 let crate::RetainedReplayAuthority::InstalledSnapshot(authority) = baseline.authority else {
769 return Err(DbError::Message(
770 "Store publication retirement requires an installed snapshot".to_string(),
771 ));
772 };
773 let entries = load_store_publication_entries_on(transaction)?;
774 let installed = entries.iter().any(|entry| {
775 entry.prepared.reference() == &snapshot.object
776 && entry.value.position == snapshot.position
777 && entry.value.entry_hash() == snapshot.entry_hash
778 && entry.value.previous_state_hash == authority.metadata.publication_predecessor.state_hash()
779 && matches!(&entry.value.payload, coven_protocol::store_commit::StorePublicationPayload::Snapshot(reference) if reference == &authority.snapshot)
780 });
781 if !installed {
782 return Err(DbError::Message(
783 "Store publication prefix retirement requires the installed accepted snapshot"
784 .to_string(),
785 ));
786 }
787 let superseded = store.retire_snapshot_artifact_ownership(
788 lookup,
789 &authority.store_root,
790 &coven_protocol::store_commit::AcceptedStoreSnapshotRef {
791 snapshot: authority.snapshot.clone(),
792 publication: snapshot.clone(),
793 },
794 &authority.metadata,
795 )?;
796 store.retire_store_blob_snapshot_ownership(authority.snapshot.object.slot(), &superseded)?;
797 let position = i64::try_from(snapshot.position.get()).map_err(|_| {
798 DbError::Message("Store publication position exceeds SQLite integer".to_string())
799 })?;
800 transaction
801 .execute(
802 "DELETE FROM store_publication_entries WHERE position < ?1",
803 [position],
804 )
805 .map_err(DbError::from)?;
806 load_store_publication_entries_on(transaction)?;
807 Ok(())
808}
809
810pub(super) fn install_accepted_store_commit_interval_on(
811 transaction: &rusqlite::Transaction<'_>,
812 accepted: &AcceptedStorePublicationInterval,
813 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
814) -> Result<AcceptedStoreCommitPublication, DbError> {
815 let previous = load_store_current_publication_on(transaction)?;
816 let publication = accepted
817 .accepted_commit(commit)
818 .map_err(|error| DbError::context("accepted Store commit publication", error))?;
819 install_store_publication_interval_on(transaction, &previous, accepted)?;
820 Ok(publication)
821}
822
823fn load_store_publication_entry_objects_on(
824 connection: &rusqlite::Connection,
825) -> Result<
826 Vec<
827 coven_protocol::objects::ExactProtocolObject<
828 coven_protocol::store_commit::StorePublicationEntry,
829 >,
830 >,
831 DbError,
832> {
833 let rows = crate::query_mapped_rows(
834 connection,
835 "SELECT position, entry_ref, entry_bytes
836 FROM store_publication_entries ORDER BY position",
837 [],
838 |row| {
839 Ok((
840 row.get::<_, i64>(0)?,
841 row.get::<_, String>(1)?,
842 row.get::<_, Vec<u8>>(2)?,
843 ))
844 },
845 )?;
846 let mut entries = Vec::with_capacity(rows.len());
847 for (position, encoded_reference, bytes) in rows {
848 let reference: coven_protocol::store_commit::StorePublicationRef =
849 serde_json::from_str(&encoded_reference)
850 .map_err(|error| DbError::context("Store publication reference", error))?;
851 if i64::try_from(reference.position.get()).ok() != Some(position) {
852 return Err(DbError::Message(
853 "Store publication entry position differs from its index".to_string(),
854 ));
855 }
856 let value: coven_protocol::store_commit::StorePublicationEntry =
857 serde_json::from_slice(&bytes)
858 .map_err(|error| DbError::context("Store publication entry", error))?;
859 if value.to_bytes() != bytes {
860 return Err(DbError::Message(
861 "Store publication entry bytes are not canonical".to_string(),
862 ));
863 }
864 let verified_reference = coven_protocol::store_commit::StorePublicationRef::from_entry(
865 &value,
866 reference.object.clone(),
867 )
868 .map_err(|error| DbError::context("Store publication entry reference", error))?;
869 if verified_reference != reference {
870 return Err(DbError::Message(
871 "Store publication entry differs from its exact reference".to_string(),
872 ));
873 }
874 let prepared = coven_protocol::objects::PreparedExactObject::new(
875 reference.object.clone(),
876 bytes.clone(),
877 )
878 .map_err(DbError::from)?;
879 entries.push(coven_protocol::objects::ExactProtocolObject {
880 value,
881 bytes,
882 prepared,
883 });
884 }
885 Ok(entries)
886}
887
888pub(super) fn load_store_publication_entries_on(
889 connection: &rusqlite::Connection,
890) -> Result<
891 Vec<
892 coven_protocol::objects::ExactProtocolObject<
893 coven_protocol::store_commit::StorePublicationEntry,
894 >,
895 >,
896 DbError,
897> {
898 let entries = load_store_publication_entry_objects_on(connection)?;
899 let current = load_store_current_publication_on(connection)?;
900 match (current.record().accepted(), entries.first(), entries.last()) {
901 (None, None, None) => {}
902 (Some(accepted), Some(first), Some(last)) => {
903 let last_reference = coven_protocol::store_commit::StorePublicationRef::from_entry(
904 &last.value,
905 last.prepared.reference().clone(),
906 )
907 .map_err(|error| DbError::context("last Store publication entry", error))?;
908 if &last_reference != accepted {
909 return Err(DbError::Message(
910 "retained Store publication interval does not reach current".to_string(),
911 ));
912 }
913 let first_reference = coven_protocol::store_commit::StorePublicationRef::from_entry(
914 &first.value,
915 first.prepared.reference().clone(),
916 )
917 .map_err(|error| DbError::context("first Store publication entry", error))?;
918 if first.value.predecessor.is_some()
919 && !matches!(
920 &first.value.payload,
921 coven_protocol::store_commit::StorePublicationPayload::Snapshot(_)
922 )
923 {
924 return Err(DbError::Message(format!(
925 "retained Store publication interval begins at non-snapshot {first_reference:?}"
926 )));
927 }
928 for pair in entries.windows(2) {
929 let predecessor = coven_protocol::store_commit::StorePublicationRef::from_entry(
930 &pair[0].value,
931 pair[0].prepared.reference().clone(),
932 )
933 .map_err(|error| {
934 DbError::context("Store publication interval predecessor", error)
935 })?;
936 if pair[1].value.predecessor.as_ref() != Some(&predecessor) {
937 return Err(DbError::Message(
938 "retained Store publication interval is not contiguous".to_string(),
939 ));
940 }
941 }
942 }
943 _ => {
944 return Err(DbError::Message(
945 "Store publication current and retained interval disagree".to_string(),
946 ));
947 }
948 }
949 Ok(entries)
950}
951
952pub(super) fn load_accepted_store_commit_on(
953 connection: &rusqlite::Connection,
954 commit: &coven_protocol::store_commit::VerifiedStoreBatchCommit,
955 publisher_signing_pubkey: &str,
956) -> Result<AcceptedStoreCommitPublication, DbError> {
957 let entries = load_store_publication_entries_on(connection)?;
958 let mut matches = entries.into_iter().filter(|candidate| {
959 matches!(
960 &candidate.value.payload,
961 coven_protocol::store_commit::StorePublicationPayload::Commit(reference)
962 if reference == commit.reference()
963 )
964 });
965 let entry = matches.next().ok_or_else(|| {
966 DbError::Message("accepted Store publication is outside the retained interval".to_string())
967 })?;
968 if matches.next().is_some() {
969 return Err(DbError::Message(
970 "Store commit appears more than once in the accepted publication interval".to_string(),
971 ));
972 }
973 let reference = coven_protocol::store_commit::StorePublicationRef::from_entry(
974 &entry.value,
975 entry.prepared.reference().clone(),
976 )
977 .map_err(DbError::from)?;
978 let publication = coven_protocol::store_commit::StoreCommitPublication::verified(
979 entry.value,
980 reference,
981 commit,
982 publisher_signing_pubkey,
983 )
984 .map_err(DbError::from)?;
985 Ok(AcceptedStoreCommitPublication { publication })
986}
987
988impl StoreSession<'_> {
989 fn store_publication_boundary(&self) -> Result<Option<StorePublicationBoundary>, DbError> {
990 let observed = load_store_publication_boundary_on(self.conn)?;
991 if observed.is_none() {
992 require_unobserved_genesis_publication(self.conn, self.store_dir)?;
993 }
994 Ok(observed)
995 }
996
997 pub(crate) fn store_current_publication(&self) -> Result<StorePublicationBoundary, DbError> {
998 load_store_current_publication_on(self.conn)
999 }
1000
1001 pub(crate) fn store_publication_entries(
1002 &self,
1003 ) -> Result<
1004 Vec<
1005 coven_protocol::objects::ExactProtocolObject<
1006 coven_protocol::store_commit::StorePublicationEntry,
1007 >,
1008 >,
1009 DbError,
1010 > {
1011 load_store_publication_entries_on(self.conn)
1012 }
1013}
1014
1015impl super::StoreDatabase {
1016 pub async fn retained_store_replay(
1020 &self,
1021 root: coven_protocol::store_commit::StoreRootRef,
1022 ) -> Result<
1023 (
1024 crate::InstalledReplayBaseline,
1025 Option<StorePublicationBoundary>,
1026 Vec<
1027 coven_protocol::objects::ExactProtocolObject<
1028 coven_protocol::store_commit::StorePublicationEntry,
1029 >,
1030 >,
1031 Vec<crate::OwnedVerifiedMergeMaterialization>,
1032 coven_protocol::store_commit::CommitFrontier,
1033 ),
1034 DbError,
1035 > {
1036 self.call_store(move |session| {
1037 let baseline = session.installed_replay_baseline()?;
1038 let observed = session.store_publication_boundary()?;
1039 let entries = match &observed {
1040 Some(_) => session.store_publication_entries()?,
1041 None => Vec::new(),
1042 };
1043 let inputs = session.retained_merge_replay_inputs(root)?;
1044 let materialized = coven_protocol::store_commit::CommitFrontier::from_refs(
1045 super::StoreRecords::new(session.conn, session.store_dir)
1046 .materialized_frontier()?,
1047 )?;
1048 Ok((baseline, observed, entries, inputs, materialized))
1049 })
1050 .await
1051 }
1052
1053 pub async fn installed_store_commit_evidence(
1054 &self,
1055 commit: coven_protocol::store_commit::VerifiedStoreBatchCommit,
1056 ) -> Result<Option<AcceptedStoreCommitEvidence>, DbError> {
1057 self.call_store(move |session| {
1058 let transaction = session
1059 .conn
1060 .unchecked_transaction()
1061 .map_err(DbError::from)?;
1062 installed_store_commit_evidence_on(
1063 super::StoreTransaction::new(&transaction, session.store_dir),
1064 &commit,
1065 )
1066 })
1067 .await
1068 }
1069
1070 pub async fn retained_store_publication(
1071 &self,
1072 ) -> Result<
1073 (
1074 StorePublicationBoundary,
1075 Vec<
1076 coven_protocol::objects::ExactProtocolObject<
1077 coven_protocol::store_commit::StorePublicationEntry,
1078 >,
1079 >,
1080 ),
1081 DbError,
1082 > {
1083 self.call_store(|session| {
1084 Ok((
1085 session.store_current_publication()?,
1086 session.store_publication_entries()?,
1087 ))
1088 })
1089 .await
1090 }
1091
1092 pub async fn store_publication_boundary(
1093 &self,
1094 ) -> Result<Option<StorePublicationBoundary>, DbError> {
1095 self.call_store(|session| session.store_publication_boundary())
1096 .await
1097 }
1098}