Skip to main content

coven_protocol/store_commit/
publication.rs

1use super::*;
2
3pub fn store_current_publication_semantic_prefix() -> &'static str {
4    "store-v1/publications/current"
5}
6
7pub fn store_current_publication_logical_key() -> &'static str {
8    "store-v1/publications/current.json"
9}
10
11pub fn store_publication_entry_semantic_prefix(entry: &StorePublicationEntry) -> String {
12    publication_entry_prefix(entry.position, entry.entry_hash())
13}
14
15fn publication_entry_prefix(position: StorePublicationPosition, hash: ObjectHash) -> String {
16    format!("store-v1/publications/entries/{}/{hash}", position.get())
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct StorePublicationPosition(u64);
22
23impl StorePublicationPosition {
24    pub fn new(value: u64) -> Result<Self, StoreProtocolError> {
25        if value == 0 {
26            return Err(StoreProtocolError::InvalidSequence(value));
27        }
28        Ok(Self(value))
29    }
30
31    pub fn get(self) -> u64 {
32        self.0
33    }
34
35    fn successor(self) -> Result<Self, StoreProtocolError> {
36        self.0
37            .checked_add(1)
38            .ok_or_else(|| {
39                StoreProtocolError::Malformed("Store publication position overflow".to_string())
40            })
41            .and_then(Self::new)
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct StorePublicationRef {
48    pub store_root_hash: ObjectHash,
49    pub position: StorePublicationPosition,
50    pub entry_hash: ObjectHash,
51    pub object: ExactObjectRef,
52}
53
54impl StorePublicationRef {
55    pub fn validate_slot(&self) -> Result<(), StoreProtocolError> {
56        let expected = format!(
57            "{}.json",
58            publication_entry_prefix(self.position, self.entry_hash)
59        );
60        if self.object.slot().logical_key() != expected {
61            return Err(StoreProtocolError::RelocatedSlot {
62                expected,
63                actual: self.object.slot().logical_key().into(),
64            });
65        }
66        Ok(())
67    }
68
69    pub fn from_entry(
70        entry: &StorePublicationEntry,
71        object: ExactObjectRef,
72    ) -> Result<Self, StoreProtocolError> {
73        entry.validate_shape()?;
74        object.verify(&entry.to_bytes())?;
75        let expected_key = format!("{}.json", store_publication_entry_semantic_prefix(entry));
76        if object.slot().logical_key() != expected_key {
77            return Err(StoreProtocolError::RelocatedSlot {
78                expected: expected_key,
79                actual: object.slot().logical_key().to_string(),
80            });
81        }
82        Ok(Self {
83            store_root_hash: entry.store_root_hash,
84            position: entry.position,
85            entry_hash: entry.entry_hash(),
86            object,
87        })
88    }
89
90    fn verify_entry(&self, entry: &StorePublicationEntry) -> Result<(), StoreProtocolError> {
91        self.object.verify(&entry.to_bytes())?;
92        if self.store_root_hash != entry.store_root_hash
93            || self.position != entry.position
94            || self.entry_hash != entry.entry_hash()
95        {
96            return Err(StoreProtocolError::Malformed(
97                "Store publication reference differs from its entry".to_string(),
98            ));
99        }
100        Ok(())
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct AcceptedStoreSnapshotRef {
107    pub snapshot: StoreSnapshotRef,
108    pub publication: StorePublicationRef,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case", deny_unknown_fields)]
113pub enum StorePublicationBase {
114    Genesis,
115    Snapshot(AcceptedStoreSnapshotRef),
116}
117
118impl StorePublicationBase {
119    pub fn validate_for_store(
120        &self,
121        expected_store_root_hash: ObjectHash,
122    ) -> Result<(), StoreProtocolError> {
123        if let Self::Snapshot(snapshot) = self {
124            crate::objects::verify_store_root(
125                expected_store_root_hash,
126                snapshot.publication.store_root_hash,
127            )?;
128        }
129        Ok(())
130    }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case", deny_unknown_fields)]
135pub enum StorePublicationPayload {
136    Commit(StoreBatchCommitRef),
137    Snapshot(StoreSnapshotRef),
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct StorePublicationEntryBody {
143    pub store_root_hash: ObjectHash,
144    pub position: StorePublicationPosition,
145    pub predecessor: Option<StorePublicationRef>,
146    pub previous_state_hash: ObjectHash,
147    pub author_registration: StoreDeviceRegistrationRef,
148    pub payload: StorePublicationPayload,
149}
150
151impl SignedBody for StorePublicationEntryBody {
152    const DOMAIN: &'static [u8] = STORE_PUBLICATION_ENTRY_DOMAIN;
153}
154
155pub type StorePublicationEntry = Signed<StorePublicationEntryBody>;
156
157impl StorePublicationEntry {
158    pub fn signed_commit(
159        current: &StoreCurrentPublicationRecord,
160        commit: &VerifiedStoreBatchCommit,
161        signer: &UserKeypair,
162    ) -> Result<Self, StoreProtocolError> {
163        let entry = Self::signed_payload(
164            current,
165            commit.author_registration.clone(),
166            StorePublicationPayload::Commit(commit.reference().clone()),
167            signer,
168        )?;
169        entry.validate_commit_against(current, commit, &keys::public_key_hex(signer))?;
170        Ok(entry)
171    }
172
173    pub fn signed_snapshot(
174        current: &StoreCurrentPublicationRecord,
175        author_registration: StoreDeviceRegistrationRef,
176        snapshot: StoreSnapshotRef,
177        signer: &UserKeypair,
178    ) -> Result<Self, StoreProtocolError> {
179        Self::signed_payload(
180            current,
181            author_registration,
182            StorePublicationPayload::Snapshot(snapshot),
183            signer,
184        )
185    }
186
187    fn signed_payload(
188        current: &StoreCurrentPublicationRecord,
189        author_registration: StoreDeviceRegistrationRef,
190        payload: StorePublicationPayload,
191        signer: &UserKeypair,
192    ) -> Result<Self, StoreProtocolError> {
193        let position = current.next_position()?;
194        let entry = Signed::sign(
195            StorePublicationEntryBody {
196                store_root_hash: current.store_root_hash,
197                position,
198                predecessor: current.accepted().cloned(),
199                previous_state_hash: current.state_hash(),
200                author_registration,
201                payload,
202            },
203            signer,
204        );
205        entry.validate_against(current.body())?;
206        Ok(entry)
207    }
208
209    pub fn entry_hash(&self) -> ObjectHash {
210        self.hash()
211    }
212
213    pub fn parse_at(
214        bytes: &[u8],
215        expected_store_root_hash: ObjectHash,
216        reference: &StorePublicationRef,
217        expected_signing_pubkey: &str,
218    ) -> Result<Self, StoreProtocolError> {
219        let entry: Self = crate::objects::decode_protocol_object(bytes)?;
220        entry.require_version()?;
221        entry.verify_by(expected_signing_pubkey)?;
222        if entry.store_root_hash != expected_store_root_hash {
223            return Err(StoreProtocolError::StoreRootMismatch {
224                expected: expected_store_root_hash,
225                actual: entry.store_root_hash,
226            });
227        }
228        reference.verify_entry(&entry)?;
229        entry.validate_shape()?;
230        Ok(entry)
231    }
232
233    fn validate_against(
234        &self,
235        current: &StoreCurrentPublicationRecordBody,
236    ) -> Result<(), StoreProtocolError> {
237        self.validate_shape()?;
238        if self.store_root_hash != current.store_root_hash
239            || self.predecessor.as_ref() != current.accepted()
240            || self.position != current.next_position()?
241            || self.previous_state_hash != current.state_hash()
242        {
243            return Err(StoreProtocolError::Malformed(
244                "Store publication entry does not extend the current accepted boundary".to_string(),
245            ));
246        }
247        Ok(())
248    }
249
250    fn validate_commit_against(
251        &self,
252        current: &StoreCurrentPublicationRecord,
253        commit: &VerifiedStoreBatchCommit,
254        publisher_signing_pubkey: &str,
255    ) -> Result<(), StoreProtocolError> {
256        self.validate_against(current.body())?;
257        let StorePublicationPayload::Commit(reference) = &self.payload else {
258            return Err(StoreProtocolError::Malformed(
259                "Store publication entry is not a commit".to_string(),
260            ));
261        };
262        reference.verify_commit(commit.value())?;
263        commit.value().verify_by(publisher_signing_pubkey)?;
264        if commit.store_root_hash() != self.store_root_hash
265            || commit.author_registration != self.author_registration
266            || commit.publication_base() != &current.publication_base()
267        {
268            return Err(StoreProtocolError::Malformed(
269                "Store commit differs from its accepted publication boundary".to_string(),
270            ));
271        }
272        Ok(())
273    }
274
275    fn validate_commit_against_record(
276        &self,
277        commit: &VerifiedStoreBatchCommit,
278        publisher_signing_pubkey: &str,
279    ) -> Result<(), StoreProtocolError> {
280        self.validate_shape()?;
281        let StorePublicationPayload::Commit(reference) = &self.payload else {
282            return Err(StoreProtocolError::Malformed(
283                "Store publication entry is not a commit".to_string(),
284            ));
285        };
286        reference.verify_commit(commit.value())?;
287        commit.value().verify_by(publisher_signing_pubkey)?;
288        commit
289            .publication_base()
290            .validate_for_store(self.store_root_hash)?;
291        if commit.store_root_hash() != self.store_root_hash
292            || commit.author_registration != self.author_registration
293        {
294            return Err(StoreProtocolError::Malformed(
295                "Store commit differs from its publication entry".to_string(),
296            ));
297        }
298        Ok(())
299    }
300
301    pub fn verify_published_commit(
302        &self,
303        commit: &VerifiedStoreBatchCommit,
304        publisher_signing_pubkey: &str,
305    ) -> Result<(), StoreProtocolError> {
306        self.validate_commit_against_record(commit, publisher_signing_pubkey)
307    }
308
309    fn validate_shape(&self) -> Result<(), StoreProtocolError> {
310        self.position.get().checked_add(1).ok_or_else(|| {
311            StoreProtocolError::Malformed("Store publication position overflow".to_string())
312        })?;
313        let expected_position = match &self.predecessor {
314            Some(predecessor) => {
315                if predecessor.store_root_hash != self.store_root_hash {
316                    return Err(StoreProtocolError::StoreRootMismatch {
317                        expected: self.store_root_hash,
318                        actual: predecessor.store_root_hash,
319                    });
320                }
321                predecessor.position.successor()?
322            }
323            None => StorePublicationPosition::new(1)?,
324        };
325        if self.position != expected_position {
326            return Err(StoreProtocolError::Malformed(
327                "Store publication entry position is not its predecessor's successor".to_string(),
328            ));
329        }
330        Ok(())
331    }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct StoreCommitPublication {
336    entry: StorePublicationEntry,
337    reference: StorePublicationRef,
338}
339
340impl StoreCommitPublication {
341    pub fn verified(
342        entry: StorePublicationEntry,
343        reference: StorePublicationRef,
344        commit: &VerifiedStoreBatchCommit,
345        publisher_signing_pubkey: &str,
346    ) -> Result<Self, StoreProtocolError> {
347        reference.verify_entry(&entry)?;
348        entry.verify_published_commit(commit, publisher_signing_pubkey)?;
349        Ok(Self { entry, reference })
350    }
351
352    pub fn entry(&self) -> &StorePublicationEntry {
353        &self.entry
354    }
355
356    pub fn reference(&self) -> &StorePublicationRef {
357        &self.reference
358    }
359
360    pub fn into_parts(self) -> (StorePublicationEntry, StorePublicationRef) {
361        (self.entry, self.reference)
362    }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct StorePublicationIntervalEntry {
367    entry: StorePublicationEntry,
368    reference: StorePublicationRef,
369    author: ReferencedStoreDeviceRegistration,
370}
371
372impl StorePublicationIntervalEntry {
373    pub fn new(
374        entry: StorePublicationEntry,
375        reference: StorePublicationRef,
376        author: ReferencedStoreDeviceRegistration,
377    ) -> Self {
378        Self {
379            entry,
380            reference,
381            author,
382        }
383    }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct AcceptedStorePublicationEntry {
388    entry: StorePublicationEntry,
389    reference: StorePublicationRef,
390    author: ReferencedStoreDeviceRegistration,
391}
392
393impl AcceptedStorePublicationEntry {
394    pub fn entry(&self) -> &StorePublicationEntry {
395        &self.entry
396    }
397
398    pub fn reference(&self) -> &StorePublicationRef {
399        &self.reference
400    }
401
402    pub fn author(&self) -> &ReferencedStoreDeviceRegistration {
403        &self.author
404    }
405
406    fn accepted_commit(
407        &self,
408        commit: &VerifiedStoreBatchCommit,
409    ) -> Result<AcceptedStoreCommitPublication, StoreProtocolError> {
410        let publication = StoreCommitPublication::verified(
411            self.entry.clone(),
412            self.reference.clone(),
413            commit,
414            &self.author.value().device_signing_pubkey,
415        )?;
416        Ok(AcceptedStoreCommitPublication { publication })
417    }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct AcceptedStoreCommitPublication {
422    publication: StoreCommitPublication,
423}
424
425impl AcceptedStoreCommitPublication {
426    pub fn entry(&self) -> &StorePublicationEntry {
427        self.publication.entry()
428    }
429
430    pub fn reference(&self) -> &StorePublicationRef {
431        self.publication.reference()
432    }
433
434    pub fn into_publication(self) -> StoreCommitPublication {
435        self.publication
436    }
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub struct VerifiedStorePublicationInterval {
441    previous: StoreCurrentPublicationRecordBody,
442    current: StoreCurrentPublicationRecord,
443    entries: Vec<AcceptedStorePublicationEntry>,
444}
445
446impl VerifiedStorePublicationInterval {
447    /// Continue from an already authenticated record. An empty interval must
448    /// preserve that exact record, including its signature.
449    pub fn verified(
450        previous: StoreCurrentPublicationRecord,
451        current: StoreCurrentPublicationRecord,
452        entries: Vec<StorePublicationIntervalEntry>,
453    ) -> Result<Self, StoreProtocolError> {
454        if entries.is_empty() {
455            if current != previous {
456                return Err(StoreProtocolError::Malformed(
457                    "empty Store publication interval changes the accepted boundary".to_string(),
458                ));
459            }
460            return Ok(Self {
461                previous: previous.body().clone(),
462                current,
463                entries: Vec::new(),
464            });
465        }
466
467        Self::from_nonempty_history(previous.body().clone(), current, entries)
468    }
469
470    /// A carried historical interval authenticates its starting state through
471    /// the first entry's state hash and its terminal signed record. It carries
472    /// no provider observation or conditional publication authority.
473    pub fn from_nonempty_history(
474        previous: StoreCurrentPublicationRecordBody,
475        current: StoreCurrentPublicationRecord,
476        entries: Vec<StorePublicationIntervalEntry>,
477    ) -> Result<Self, StoreProtocolError> {
478        if entries.is_empty() {
479            return Err(StoreProtocolError::Malformed(
480                "carried Store publication history has no entries".into(),
481            ));
482        }
483        Self::fold(previous, current, entries)
484    }
485
486    /// Start from the pinned Store root without requiring its founder's private
487    /// key. An empty Store still authenticates the actual remote genesis record.
488    pub fn from_genesis(
489        store_root_hash: ObjectHash,
490        founder_pubkey: &str,
491        current: StoreCurrentPublicationRecord,
492        entries: Vec<StorePublicationIntervalEntry>,
493    ) -> Result<Self, StoreProtocolError> {
494        let previous = StoreCurrentPublicationRecordBody::genesis(store_root_hash);
495        if entries.is_empty() {
496            current.verify_genesis(store_root_hash, founder_pubkey)?;
497            return Ok(Self {
498                previous,
499                current,
500                entries: Vec::new(),
501            });
502        }
503        Self::fold(previous, current, entries)
504    }
505
506    fn fold(
507        previous: StoreCurrentPublicationRecordBody,
508        current: StoreCurrentPublicationRecord,
509        entries: Vec<StorePublicationIntervalEntry>,
510    ) -> Result<Self, StoreProtocolError> {
511        let mut folded = previous.clone();
512        let mut accepted = Vec::with_capacity(entries.len());
513        let mut commit_coordinates = std::collections::BTreeSet::new();
514        let mut final_publisher = None;
515        for candidate in entries {
516            let registration_bytes = candidate.author.value().to_bytes();
517            candidate
518                .author
519                .reference()
520                .object
521                .verify(&registration_bytes)?;
522            let parsed_registration = StoreDeviceRegistration::parse_at(
523                &registration_bytes,
524                &candidate.author.value().store_root,
525                candidate.author.reference().device_id,
526            )?;
527            candidate
528                .author
529                .reference()
530                .verify_registration(&parsed_registration)?;
531            if parsed_registration != *candidate.author.value()
532                || candidate.author.value().store_root.store_root_hash != folded.store_root_hash
533                || candidate.entry.author_registration != *candidate.author.reference()
534            {
535                return Err(StoreProtocolError::Malformed(
536                    "Store publication entry differs from its exact author registration"
537                        .to_string(),
538                ));
539            }
540            let parsed_entry = StorePublicationEntry::parse_at(
541                &candidate.entry.to_bytes(),
542                folded.store_root_hash,
543                &candidate.reference,
544                &candidate.author.value().device_signing_pubkey,
545            )?;
546            if parsed_entry != candidate.entry {
547                return Err(StoreProtocolError::Malformed(
548                    "Store publication entry differs from its canonical bytes".to_string(),
549                ));
550            }
551            if let StorePublicationPayload::Commit(commit) = &candidate.entry.payload {
552                if !commit_coordinates.insert(commit.coord.clone()) {
553                    return Err(StoreProtocolError::Malformed(
554                        "Store publication interval repeats an author sequence".to_string(),
555                    ));
556                }
557            }
558            folded = folded.advance(&candidate.entry, candidate.reference.clone())?;
559            final_publisher = Some(candidate.author.value().device_signing_pubkey.clone());
560            accepted.push(AcceptedStorePublicationEntry {
561                entry: candidate.entry,
562                reference: candidate.reference,
563                author: candidate.author,
564            });
565        }
566
567        let final_publisher = final_publisher.expect("a non-empty interval has a publisher");
568        current.verify_by(&final_publisher)?;
569        if current.body() != &folded {
570            return Err(StoreProtocolError::Malformed(
571                "Store current publication record differs from its verified interval".to_string(),
572            ));
573        }
574        Ok(Self {
575            previous,
576            current,
577            entries: accepted,
578        })
579    }
580
581    /// The authenticated starting state, derived from the prior record or root.
582    pub fn previous(&self) -> &StoreCurrentPublicationRecordBody {
583        &self.previous
584    }
585
586    pub fn current(&self) -> &StoreCurrentPublicationRecord {
587        &self.current
588    }
589
590    pub fn entries(&self) -> &[AcceptedStorePublicationEntry] {
591        &self.entries
592    }
593
594    pub fn accepted_commit(
595        &self,
596        commit: &VerifiedStoreBatchCommit,
597    ) -> Result<AcceptedStoreCommitPublication, StoreProtocolError> {
598        let mut base = self.previous.publication_base();
599        for entry in &self.entries {
600            match &entry.entry.payload {
601                StorePublicationPayload::Snapshot(snapshot) => {
602                    base = StorePublicationBase::Snapshot(AcceptedStoreSnapshotRef {
603                        snapshot: snapshot.clone(),
604                        publication: entry.reference.clone(),
605                    });
606                }
607                StorePublicationPayload::Commit(reference) if reference == commit.reference() => {
608                    if commit.publication_base() != &base {
609                        return Err(StoreProtocolError::Malformed(
610                            "Store commit differs from the snapshot base at its accepted publication"
611                                .to_string(),
612                        ));
613                    }
614                    return entry.accepted_commit(commit);
615                }
616                StorePublicationPayload::Commit(_) => {}
617            }
618        }
619        Err(StoreProtocolError::Malformed(
620            "Store commit is absent from the accepted publication interval".to_string(),
621        ))
622    }
623}
624
625#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(rename_all = "snake_case", deny_unknown_fields)]
627pub enum StorePublicationState {
628    Genesis,
629    Accepted {
630        entry: StorePublicationRef,
631        latest_snapshot: Option<AcceptedStoreSnapshotRef>,
632    },
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct StoreCurrentPublicationRecordBody {
638    pub store_root_hash: ObjectHash,
639    pub state: StorePublicationState,
640}
641
642impl StoreCurrentPublicationRecordBody {
643    pub fn genesis(store_root_hash: ObjectHash) -> Self {
644        Self {
645            store_root_hash,
646            state: StorePublicationState::Genesis,
647        }
648    }
649
650    pub fn state_hash(&self) -> ObjectHash {
651        super::signed::signed_body_hash(STORE_PROTOCOL_VERSION, self)
652    }
653
654    pub fn accepted(&self) -> Option<&StorePublicationRef> {
655        match &self.state {
656            StorePublicationState::Genesis => None,
657            StorePublicationState::Accepted { entry, .. } => Some(entry),
658        }
659    }
660
661    pub fn latest_snapshot(&self) -> Option<&AcceptedStoreSnapshotRef> {
662        match &self.state {
663            StorePublicationState::Genesis => None,
664            StorePublicationState::Accepted {
665                latest_snapshot, ..
666            } => latest_snapshot.as_ref(),
667        }
668    }
669
670    pub fn publication_base(&self) -> StorePublicationBase {
671        match self.latest_snapshot() {
672            Some(snapshot) => StorePublicationBase::Snapshot(snapshot.clone()),
673            None => StorePublicationBase::Genesis,
674        }
675    }
676
677    pub fn next_position(&self) -> Result<StorePublicationPosition, StoreProtocolError> {
678        match self.accepted() {
679            Some(reference) => reference.position.successor(),
680            None => StorePublicationPosition::new(1),
681        }
682    }
683
684    fn advance(
685        &self,
686        entry: &StorePublicationEntry,
687        reference: StorePublicationRef,
688    ) -> Result<Self, StoreProtocolError> {
689        entry.validate_against(self)?;
690        reference.verify_entry(entry)?;
691        let latest_snapshot = match &entry.payload {
692            StorePublicationPayload::Commit(_) => self.latest_snapshot().cloned(),
693            StorePublicationPayload::Snapshot(snapshot) => Some(AcceptedStoreSnapshotRef {
694                snapshot: snapshot.clone(),
695                publication: reference.clone(),
696            }),
697        };
698        Ok(Self {
699            store_root_hash: self.store_root_hash,
700            state: StorePublicationState::Accepted {
701                entry: reference,
702                latest_snapshot,
703            },
704        })
705    }
706}
707
708impl SignedBody for StoreCurrentPublicationRecordBody {
709    const DOMAIN: &'static [u8] = STORE_CURRENT_PUBLICATION_DOMAIN;
710}
711
712pub type StoreCurrentPublicationRecord = Signed<StoreCurrentPublicationRecordBody>;
713
714impl StoreCurrentPublicationRecord {
715    pub fn state_hash(&self) -> ObjectHash {
716        self.body().state_hash()
717    }
718
719    pub fn genesis(store_root_hash: ObjectHash, founder: &UserKeypair) -> Self {
720        Signed::sign(
721            StoreCurrentPublicationRecordBody::genesis(store_root_hash),
722            founder,
723        )
724    }
725
726    pub fn advance_commit(
727        previous: &Self,
728        entry: &StorePublicationEntry,
729        reference: StorePublicationRef,
730        commit: &VerifiedStoreBatchCommit,
731        signer: &UserKeypair,
732    ) -> Result<Self, StoreProtocolError> {
733        entry.validate_commit_against(previous, commit, &keys::public_key_hex(signer))?;
734        Self::advance(previous, entry, reference, signer)
735    }
736
737    pub fn advance_snapshot(
738        previous: &Self,
739        entry: &StorePublicationEntry,
740        reference: StorePublicationRef,
741        signer: &UserKeypair,
742    ) -> Result<Self, StoreProtocolError> {
743        if !matches!(entry.payload, StorePublicationPayload::Snapshot(_)) {
744            return Err(StoreProtocolError::Malformed(
745                "Store publication entry is not a snapshot".to_string(),
746            ));
747        }
748        Self::advance(previous, entry, reference, signer)
749    }
750
751    fn advance(
752        previous: &Self,
753        entry: &StorePublicationEntry,
754        reference: StorePublicationRef,
755        signer: &UserKeypair,
756    ) -> Result<Self, StoreProtocolError> {
757        Ok(Signed::sign(
758            previous.body().advance(entry, reference)?,
759            signer,
760        ))
761    }
762
763    pub fn record_hash(&self) -> ObjectHash {
764        self.hash()
765    }
766
767    pub fn accepted(&self) -> Option<&StorePublicationRef> {
768        self.body().accepted()
769    }
770
771    pub fn latest_snapshot(&self) -> Option<&AcceptedStoreSnapshotRef> {
772        self.body().latest_snapshot()
773    }
774
775    pub fn publication_base(&self) -> StorePublicationBase {
776        self.body().publication_base()
777    }
778
779    pub fn next_position(&self) -> Result<StorePublicationPosition, StoreProtocolError> {
780        self.body().next_position()
781    }
782
783    pub fn verify_genesis(
784        &self,
785        expected_store_root_hash: ObjectHash,
786        founder_pubkey: &str,
787    ) -> Result<(), StoreProtocolError> {
788        if self.store_root_hash != expected_store_root_hash
789            || self.state != StorePublicationState::Genesis
790        {
791            return Err(StoreProtocolError::Malformed(
792                "Store genesis publication record differs from its descriptor".to_string(),
793            ));
794        }
795        self.verify_by(founder_pubkey)
796    }
797
798    pub fn verify_commit_transition(
799        &self,
800        previous: &Self,
801        entry: &StorePublicationEntry,
802        reference: &StorePublicationRef,
803        commit: &VerifiedStoreBatchCommit,
804        publisher_signing_pubkey: &str,
805    ) -> Result<(), StoreProtocolError> {
806        entry.validate_commit_against(previous, commit, publisher_signing_pubkey)?;
807        self.verify_transition(previous, entry, reference, publisher_signing_pubkey)
808    }
809
810    fn verify_transition(
811        &self,
812        previous: &Self,
813        entry: &StorePublicationEntry,
814        reference: &StorePublicationRef,
815        publisher_signing_pubkey: &str,
816    ) -> Result<(), StoreProtocolError> {
817        entry.validate_against(previous.body())?;
818        reference.verify_entry(entry)?;
819        self.verify_by(publisher_signing_pubkey)?;
820        let expected_latest = match &entry.payload {
821            StorePublicationPayload::Commit(_) => previous.latest_snapshot().cloned(),
822            StorePublicationPayload::Snapshot(snapshot) => Some(AcceptedStoreSnapshotRef {
823                snapshot: snapshot.clone(),
824                publication: reference.clone(),
825            }),
826        };
827        let expected_state = StorePublicationState::Accepted {
828            entry: reference.clone(),
829            latest_snapshot: expected_latest,
830        };
831        if self.store_root_hash != previous.store_root_hash || self.state != expected_state {
832            return Err(StoreProtocolError::Malformed(
833                "Store current publication record differs from its accepted transition".to_string(),
834            ));
835        }
836        Ok(())
837    }
838
839    pub fn verify_accepted_commit(
840        &self,
841        entry: &StorePublicationEntry,
842        reference: &StorePublicationRef,
843        commit: &VerifiedStoreBatchCommit,
844        publisher_signing_pubkey: &str,
845    ) -> Result<(), StoreProtocolError> {
846        let base = commit.publication_base();
847        entry.validate_commit_against_record(commit, publisher_signing_pubkey)?;
848        self.verify_accepted_with_latest(
849            entry,
850            reference,
851            match base {
852                StorePublicationBase::Genesis => None,
853                StorePublicationBase::Snapshot(snapshot) => Some(snapshot.clone()),
854            },
855            publisher_signing_pubkey,
856        )
857    }
858
859    fn verify_accepted_with_latest(
860        &self,
861        entry: &StorePublicationEntry,
862        reference: &StorePublicationRef,
863        expected_latest: Option<AcceptedStoreSnapshotRef>,
864        publisher_signing_pubkey: &str,
865    ) -> Result<(), StoreProtocolError> {
866        entry.validate_shape()?;
867        reference.verify_entry(entry)?;
868        self.verify_by(publisher_signing_pubkey)?;
869        let expected_state = StorePublicationState::Accepted {
870            entry: reference.clone(),
871            latest_snapshot: expected_latest,
872        };
873        if self.store_root_hash != entry.store_root_hash || self.state != expected_state {
874            return Err(StoreProtocolError::Malformed(
875                "Store current publication record differs from its accepted entry".to_string(),
876            ));
877        }
878        Ok(())
879    }
880}
881
882#[cfg(test)]
883#[path = "publication_tests.rs"]
884mod tests;