Skip to main content

coven_database/store/store_session/
observed_store_publication.rs

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/// An installed accepted prefix may be handed to a joining device without a
17/// provider observation. Only an observed boundary can start a conditional write.
18#[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/// Whether this publisher advanced the provider or found its commit already installed.
147#[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    /// Resolve against the installation transaction: pull may install the
158    /// commit or replace its retained acceptance proof with a covering snapshot
159    /// after the publisher obtained its outcome.
160    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                // The pull captured its previous boundary before reading packages.
440                // Restart against the concurrent installation before making any
441                // durable changes, including acceptance and replay retirement.
442                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        // Another pull can install this exact interval while its packages are
513        // loading. Its authenticated tip already commits to the same entry
514        // chain; preserve the installed provider revision and retained prefix.
515        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
679/// Replace a retired publication prefix using an independently authenticated
680/// checkpoint. The checkpoint is the replay authority; this does not assert
681/// acceptance of any individual commit omitted by its retained interval.
682pub(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    // The old entries may have a retired gap before this checkpoint. Keeping
740    // them would represent a contiguous interval that was never verified.
741    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    /// Read the replay floor, accepted history, materialized frontier, and inputs in one
1017    /// database job. A concurrent baseline advance cannot retire the prefix
1018    /// between these reads.
1019    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}