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    format!(
13        "store-v1/publications/entries/{}/{}",
14        entry.position.get(),
15        entry.entry_hash()
16    )
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 from_entry(
56        entry: &StorePublicationEntry,
57        object: ExactObjectRef,
58    ) -> Result<Self, StoreProtocolError> {
59        entry.validate_shape()?;
60        object.verify(&entry.to_bytes())?;
61        let expected_key = format!("{}.json", store_publication_entry_semantic_prefix(entry));
62        if object.slot().logical_key() != expected_key {
63            return Err(StoreProtocolError::RelocatedSlot {
64                expected: expected_key,
65                actual: object.slot().logical_key().to_string(),
66            });
67        }
68        Ok(Self {
69            store_root_hash: entry.store_root_hash,
70            position: entry.position,
71            entry_hash: entry.entry_hash(),
72            object,
73        })
74    }
75
76    fn verify_entry(&self, entry: &StorePublicationEntry) -> Result<(), StoreProtocolError> {
77        self.object.verify(&entry.to_bytes())?;
78        if self.store_root_hash != entry.store_root_hash
79            || self.position != entry.position
80            || self.entry_hash != entry.entry_hash()
81        {
82            return Err(StoreProtocolError::Malformed(
83                "Store publication reference differs from its entry".to_string(),
84            ));
85        }
86        Ok(())
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct AcceptedStoreSnapshotRef {
93    pub snapshot: StoreSnapshotRef,
94    pub publication: StorePublicationRef,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case", deny_unknown_fields)]
99pub enum StorePublicationBase {
100    Genesis,
101    Snapshot(AcceptedStoreSnapshotRef),
102}
103
104impl StorePublicationBase {
105    pub fn validate_for_store(
106        &self,
107        expected_store_root_hash: ObjectHash,
108    ) -> Result<(), StoreProtocolError> {
109        if let Self::Snapshot(snapshot) = self {
110            crate::objects::verify_store_root(
111                expected_store_root_hash,
112                snapshot.publication.store_root_hash,
113            )?;
114        }
115        Ok(())
116    }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case", deny_unknown_fields)]
121pub enum StorePublicationPayload {
122    Commit(StoreBatchCommitRef),
123    Snapshot(StoreSnapshotRef),
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct StorePublicationEntryBody {
129    pub store_root_hash: ObjectHash,
130    pub position: StorePublicationPosition,
131    pub predecessor: Option<StorePublicationRef>,
132    pub previous_record_hash: ObjectHash,
133    pub author_registration: StoreDeviceRegistrationRef,
134    pub payload: StorePublicationPayload,
135}
136
137impl SignedBody for StorePublicationEntryBody {
138    const DOMAIN: &'static [u8] = STORE_PUBLICATION_ENTRY_DOMAIN;
139}
140
141pub type StorePublicationEntry = Signed<StorePublicationEntryBody>;
142
143impl StorePublicationEntry {
144    pub fn signed_commit(
145        current: &StoreCurrentPublicationRecord,
146        commit: &VerifiedStoreBatchCommit,
147        signer: &UserKeypair,
148    ) -> Result<Self, StoreProtocolError> {
149        let entry = Self::signed_payload(
150            current,
151            commit.author_registration.clone(),
152            StorePublicationPayload::Commit(commit.reference().clone()),
153            signer,
154        )?;
155        entry.validate_commit_against(current, commit, &keys::public_key_hex(signer))?;
156        Ok(entry)
157    }
158
159    pub fn signed_snapshot(
160        current: &StoreCurrentPublicationRecord,
161        author_registration: StoreDeviceRegistrationRef,
162        snapshot: StoreSnapshotRef,
163        signer: &UserKeypair,
164    ) -> Result<Self, StoreProtocolError> {
165        current.accepted().ok_or_else(|| {
166            StoreProtocolError::Malformed(
167                "Store snapshot cannot cover an empty publication history".to_string(),
168            )
169        })?;
170        Self::signed_payload(
171            current,
172            author_registration,
173            StorePublicationPayload::Snapshot(snapshot),
174            signer,
175        )
176    }
177
178    fn signed_payload(
179        current: &StoreCurrentPublicationRecord,
180        author_registration: StoreDeviceRegistrationRef,
181        payload: StorePublicationPayload,
182        signer: &UserKeypair,
183    ) -> Result<Self, StoreProtocolError> {
184        let position = current.next_position()?;
185        let entry = Signed::sign(
186            StorePublicationEntryBody {
187                store_root_hash: current.store_root_hash,
188                position,
189                predecessor: current.accepted().cloned(),
190                previous_record_hash: current.record_hash(),
191                author_registration,
192                payload,
193            },
194            signer,
195        );
196        entry.validate_against(current)?;
197        Ok(entry)
198    }
199
200    pub fn entry_hash(&self) -> ObjectHash {
201        self.hash()
202    }
203
204    pub fn parse_at(
205        bytes: &[u8],
206        expected_store_root_hash: ObjectHash,
207        reference: &StorePublicationRef,
208        expected_signing_pubkey: &str,
209    ) -> Result<Self, StoreProtocolError> {
210        let entry: Self = crate::objects::decode_protocol_object(bytes)?;
211        entry.require_version()?;
212        entry.verify_by(expected_signing_pubkey)?;
213        if entry.store_root_hash != expected_store_root_hash {
214            return Err(StoreProtocolError::StoreRootMismatch {
215                expected: expected_store_root_hash,
216                actual: entry.store_root_hash,
217            });
218        }
219        reference.verify_entry(&entry)?;
220        entry.validate_shape()?;
221        Ok(entry)
222    }
223
224    fn validate_against(
225        &self,
226        current: &StoreCurrentPublicationRecord,
227    ) -> Result<(), StoreProtocolError> {
228        self.validate_shape()?;
229        if self.store_root_hash != current.store_root_hash
230            || self.predecessor.as_ref() != current.accepted()
231            || self.position != current.next_position()?
232            || self.previous_record_hash != current.record_hash()
233        {
234            return Err(StoreProtocolError::Malformed(
235                "Store publication entry does not extend the current accepted boundary".to_string(),
236            ));
237        }
238        Ok(())
239    }
240
241    fn validate_commit_against(
242        &self,
243        current: &StoreCurrentPublicationRecord,
244        commit: &VerifiedStoreBatchCommit,
245        publisher_signing_pubkey: &str,
246    ) -> Result<(), StoreProtocolError> {
247        self.validate_against(current)?;
248        let StorePublicationPayload::Commit(reference) = &self.payload else {
249            return Err(StoreProtocolError::Malformed(
250                "Store publication entry is not a commit".to_string(),
251            ));
252        };
253        reference.verify_commit(commit.value())?;
254        commit.value().verify_by(publisher_signing_pubkey)?;
255        if commit.store_root_hash() != self.store_root_hash
256            || commit.author_registration != self.author_registration
257            || commit.publication_base() != &current.publication_base()
258        {
259            return Err(StoreProtocolError::Malformed(
260                "Store commit differs from its accepted publication boundary".to_string(),
261            ));
262        }
263        Ok(())
264    }
265
266    fn validate_commit_against_record(
267        &self,
268        commit: &VerifiedStoreBatchCommit,
269        publisher_signing_pubkey: &str,
270    ) -> Result<(), StoreProtocolError> {
271        self.validate_shape()?;
272        let StorePublicationPayload::Commit(reference) = &self.payload else {
273            return Err(StoreProtocolError::Malformed(
274                "Store publication entry is not a commit".to_string(),
275            ));
276        };
277        reference.verify_commit(commit.value())?;
278        commit.value().verify_by(publisher_signing_pubkey)?;
279        commit
280            .publication_base()
281            .validate_for_store(self.store_root_hash)?;
282        if commit.store_root_hash() != self.store_root_hash
283            || commit.author_registration != self.author_registration
284        {
285            return Err(StoreProtocolError::Malformed(
286                "Store commit differs from its publication entry".to_string(),
287            ));
288        }
289        Ok(())
290    }
291
292    pub fn verify_published_commit(
293        &self,
294        commit: &VerifiedStoreBatchCommit,
295        publisher_signing_pubkey: &str,
296    ) -> Result<(), StoreProtocolError> {
297        self.validate_commit_against_record(commit, publisher_signing_pubkey)
298    }
299
300    fn validate_shape(&self) -> Result<(), StoreProtocolError> {
301        self.position.get().checked_add(1).ok_or_else(|| {
302            StoreProtocolError::Malformed("Store publication position overflow".to_string())
303        })?;
304        let expected_position = match &self.predecessor {
305            Some(predecessor) => {
306                if predecessor.store_root_hash != self.store_root_hash {
307                    return Err(StoreProtocolError::StoreRootMismatch {
308                        expected: self.store_root_hash,
309                        actual: predecessor.store_root_hash,
310                    });
311                }
312                predecessor.position.successor()?
313            }
314            None => StorePublicationPosition::new(1)?,
315        };
316        if self.position != expected_position {
317            return Err(StoreProtocolError::Malformed(
318                "Store publication entry position is not its predecessor's successor".to_string(),
319            ));
320        }
321        Ok(())
322    }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(rename_all = "snake_case", deny_unknown_fields)]
327pub enum StorePublicationState {
328    Genesis,
329    Accepted {
330        entry: StorePublicationRef,
331        latest_snapshot: Option<AcceptedStoreSnapshotRef>,
332    },
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(deny_unknown_fields)]
337pub struct StoreCurrentPublicationRecordBody {
338    pub store_root_hash: ObjectHash,
339    pub state: StorePublicationState,
340}
341
342impl SignedBody for StoreCurrentPublicationRecordBody {
343    const DOMAIN: &'static [u8] = STORE_CURRENT_PUBLICATION_DOMAIN;
344}
345
346pub type StoreCurrentPublicationRecord = Signed<StoreCurrentPublicationRecordBody>;
347
348impl StoreCurrentPublicationRecord {
349    pub fn genesis(store_root_hash: ObjectHash, founder: &UserKeypair) -> Self {
350        Signed::sign(
351            StoreCurrentPublicationRecordBody {
352                store_root_hash,
353                state: StorePublicationState::Genesis,
354            },
355            founder,
356        )
357    }
358
359    pub fn advance_commit(
360        previous: &Self,
361        entry: &StorePublicationEntry,
362        reference: StorePublicationRef,
363        commit: &VerifiedStoreBatchCommit,
364        signer: &UserKeypair,
365    ) -> Result<Self, StoreProtocolError> {
366        entry.validate_commit_against(previous, commit, &keys::public_key_hex(signer))?;
367        Self::advance(previous, entry, reference, signer)
368    }
369
370    pub fn advance_snapshot(
371        previous: &Self,
372        entry: &StorePublicationEntry,
373        reference: StorePublicationRef,
374        signer: &UserKeypair,
375    ) -> Result<Self, StoreProtocolError> {
376        if !matches!(entry.payload, StorePublicationPayload::Snapshot(_)) {
377            return Err(StoreProtocolError::Malformed(
378                "Store publication entry is not a snapshot".to_string(),
379            ));
380        }
381        Self::advance(previous, entry, reference, signer)
382    }
383
384    fn advance(
385        previous: &Self,
386        entry: &StorePublicationEntry,
387        reference: StorePublicationRef,
388        signer: &UserKeypair,
389    ) -> Result<Self, StoreProtocolError> {
390        entry.validate_against(previous)?;
391        reference.verify_entry(entry)?;
392        let latest_snapshot = match &entry.payload {
393            StorePublicationPayload::Commit(_) => previous.latest_snapshot().cloned(),
394            StorePublicationPayload::Snapshot(snapshot) => Some(AcceptedStoreSnapshotRef {
395                snapshot: snapshot.clone(),
396                publication: reference.clone(),
397            }),
398        };
399        Ok(Signed::sign(
400            StoreCurrentPublicationRecordBody {
401                store_root_hash: previous.store_root_hash,
402                state: StorePublicationState::Accepted {
403                    entry: reference,
404                    latest_snapshot,
405                },
406            },
407            signer,
408        ))
409    }
410
411    pub fn record_hash(&self) -> ObjectHash {
412        self.hash()
413    }
414
415    pub fn accepted(&self) -> Option<&StorePublicationRef> {
416        match &self.state {
417            StorePublicationState::Genesis => None,
418            StorePublicationState::Accepted { entry, .. } => Some(entry),
419        }
420    }
421
422    pub fn latest_snapshot(&self) -> Option<&AcceptedStoreSnapshotRef> {
423        match &self.state {
424            StorePublicationState::Genesis => None,
425            StorePublicationState::Accepted {
426                latest_snapshot, ..
427            } => latest_snapshot.as_ref(),
428        }
429    }
430
431    pub fn publication_base(&self) -> StorePublicationBase {
432        match self.latest_snapshot() {
433            Some(snapshot) => StorePublicationBase::Snapshot(snapshot.clone()),
434            None => StorePublicationBase::Genesis,
435        }
436    }
437
438    pub fn next_position(&self) -> Result<StorePublicationPosition, StoreProtocolError> {
439        match self.accepted() {
440            Some(reference) => reference.position.successor(),
441            None => StorePublicationPosition::new(1),
442        }
443    }
444
445    pub fn verify_genesis(
446        &self,
447        expected_store_root_hash: ObjectHash,
448        founder_pubkey: &str,
449    ) -> Result<(), StoreProtocolError> {
450        if self.store_root_hash != expected_store_root_hash
451            || self.state != StorePublicationState::Genesis
452        {
453            return Err(StoreProtocolError::Malformed(
454                "Store genesis publication record differs from its descriptor".to_string(),
455            ));
456        }
457        self.verify_by(founder_pubkey)
458    }
459
460    pub fn verify_commit_transition(
461        &self,
462        previous: &Self,
463        entry: &StorePublicationEntry,
464        reference: &StorePublicationRef,
465        commit: &VerifiedStoreBatchCommit,
466        publisher_signing_pubkey: &str,
467    ) -> Result<(), StoreProtocolError> {
468        entry.validate_commit_against(previous, commit, publisher_signing_pubkey)?;
469        self.verify_transition(previous, entry, reference, publisher_signing_pubkey)
470    }
471
472    fn verify_transition(
473        &self,
474        previous: &Self,
475        entry: &StorePublicationEntry,
476        reference: &StorePublicationRef,
477        publisher_signing_pubkey: &str,
478    ) -> Result<(), StoreProtocolError> {
479        entry.validate_against(previous)?;
480        reference.verify_entry(entry)?;
481        self.verify_by(publisher_signing_pubkey)?;
482        let expected_latest = match &entry.payload {
483            StorePublicationPayload::Commit(_) => previous.latest_snapshot().cloned(),
484            StorePublicationPayload::Snapshot(snapshot) => Some(AcceptedStoreSnapshotRef {
485                snapshot: snapshot.clone(),
486                publication: reference.clone(),
487            }),
488        };
489        let expected_state = StorePublicationState::Accepted {
490            entry: reference.clone(),
491            latest_snapshot: expected_latest,
492        };
493        if self.store_root_hash != previous.store_root_hash || self.state != expected_state {
494            return Err(StoreProtocolError::Malformed(
495                "Store current publication record differs from its accepted transition".to_string(),
496            ));
497        }
498        Ok(())
499    }
500
501    pub fn verify_accepted_commit(
502        &self,
503        entry: &StorePublicationEntry,
504        reference: &StorePublicationRef,
505        commit: &VerifiedStoreBatchCommit,
506        publisher_signing_pubkey: &str,
507    ) -> Result<(), StoreProtocolError> {
508        let base = commit.publication_base();
509        entry.validate_commit_against_record(commit, publisher_signing_pubkey)?;
510        self.verify_accepted_with_latest(
511            entry,
512            reference,
513            match base {
514                StorePublicationBase::Genesis => None,
515                StorePublicationBase::Snapshot(snapshot) => Some(snapshot.clone()),
516            },
517            publisher_signing_pubkey,
518        )
519    }
520
521    fn verify_accepted_with_latest(
522        &self,
523        entry: &StorePublicationEntry,
524        reference: &StorePublicationRef,
525        expected_latest: Option<AcceptedStoreSnapshotRef>,
526        publisher_signing_pubkey: &str,
527    ) -> Result<(), StoreProtocolError> {
528        entry.validate_shape()?;
529        reference.verify_entry(entry)?;
530        self.verify_by(publisher_signing_pubkey)?;
531        let expected_state = StorePublicationState::Accepted {
532            entry: reference.clone(),
533            latest_snapshot: expected_latest,
534        };
535        if self.store_root_hash != entry.store_root_hash || self.state != expected_state {
536            return Err(StoreProtocolError::Malformed(
537                "Store current publication record differs from its accepted entry".to_string(),
538            ));
539        }
540        Ok(())
541    }
542}
543
544#[cfg(test)]
545#[path = "publication_tests.rs"]
546mod tests;