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() != ¤t.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;