1use super::*;
2
3#[derive(Debug, thiserror::Error)]
4pub enum StoreProtocolError {
5 #[error("object hash must be exactly 64 lowercase hexadecimal characters: {0:?}")]
6 InvalidObjectHash(String),
7 #[error("unsupported Store protocol version {0}")]
8 UnsupportedVersion(u32),
9 #[error("malformed Store protocol object: {0}")]
10 Malformed(String),
11 #[error("Store protocol JSON: {0}")]
12 Json(#[from] serde_json::Error),
13 #[error("Store protocol object storage shape: {0}")]
14 Storage(#[from] crate::objects::StorageError),
15 #[error("Store protocol Circle control coordinate: {0}")]
16 CircleControlCoord(#[from] crate::circle_control::CircleControlCoordError),
17 #[error("Store provider probe: {0}")]
18 ProviderProbe(#[source] Box<crate::provider::ProviderProbeError>),
19 #[error("invalid author stream id: {0}")]
20 AuthorStreamId(#[from] crate::causal_grants::AuthorStreamIdParseError),
21 #[error("Store protocol signature is invalid")]
22 InvalidSignature,
23 #[error("Owner promotion evidence does not match its exact Store authority")]
24 OwnerPromotionMismatch,
25 #[error("Store protocol object is in slot {actual:?}, expected {expected:?}")]
26 RelocatedSlot { expected: String, actual: String },
27 #[error("Store package names key {actual:?}, expected {expected:?}")]
28 RelocatedPackage { expected: String, actual: String },
29 #[error("candidate object names key {actual:?}, expected {expected:?}")]
30 RelocatedCandidateObject { expected: String, actual: String },
31 #[error("Store protocol root hash is {actual}, expected {expected}")]
32 StoreRootMismatch {
33 expected: ObjectHash,
34 actual: ObjectHash,
35 },
36 #[error("Store protocol root id is {actual}, expected {expected}")]
37 StoreRootIdMismatch {
38 expected: ObjectHash,
39 actual: ObjectHash,
40 },
41 #[error("Store id is {actual:?}, expected {expected:?}")]
42 StoreMismatch { expected: String, actual: String },
43 #[error("founder is {actual:?}, expected {expected:?}")]
44 FounderMismatch { expected: String, actual: String },
45 #[error("store protocol root has an invalid founder membership entry")]
46 InvalidFounder,
47 #[error("Store sync-routing hash is {actual}, expected {expected}")]
48 SyncRoutingMismatch {
49 expected: ObjectHash,
50 actual: ObjectHash,
51 },
52 #[error("Store Merge membership control is invalid or signed by a different device")]
53 InvalidMergeMembershipControl,
54 #[error("Store batch has no Store package, circle package, or control")]
55 EmptyBatch,
56 #[error("Store batch has no Store package")]
57 MissingStorePackage,
58 #[error("Store batch repeats Store device registration {device_id:?}")]
59 DuplicateDeviceRegistration { device_id: String },
60 #[error("Store device registration {device_id:?} has hash {actual}, expected {expected}")]
61 DeviceRegistrationRefMismatch {
62 device_id: String,
63 expected: ObjectHash,
64 actual: ObjectHash,
65 },
66 #[error("device join attempt fields do not name one exact registration lifecycle")]
67 JoinAttemptMismatch,
68 #[error("device readiness proof differs from its exact attempt, registration, or initial acknowledgement")]
69 DeviceReadinessMismatch,
70 #[error("device join outcome differs from its exact attempt or closed outcome variant")]
71 JoinOutcomeMismatch,
72 #[error("provider access activation contains duplicate or contradictory exact authority")]
73 ProviderAccessMismatch,
74 #[error("Owner recovery node differs from its exact registration lifecycle")]
75 OwnerRecoveryMismatch,
76 #[error("Store device state differs from its signed predecessor state")]
77 DeviceStateMismatch,
78 #[error("Store batch has no package for circle {0}")]
79 MissingCirclePackage(CircleId),
80 #[error("Store batch has more than one package for circle {0}")]
81 DuplicateCirclePackage(CircleId),
82 #[error("Store batch has more than one control for circle {0}")]
83 DuplicateCircleControl(CircleId),
84 #[error("circle control coordinate is invalid")]
85 InvalidCircleControlCoord,
86 #[error("circle {circle_id} package is at {actual:?}, expected {expected:?}")]
87 RelocatedCirclePackage {
88 circle_id: CircleId,
89 expected: String,
90 actual: String,
91 },
92 #[error("Store commit sequence must start at 1, got {0}")]
93 InvalidSequence(u64),
94 #[error("Store commit sequence 1 must not name a predecessor")]
95 UnexpectedPredecessor,
96 #[error("Store commit after sequence 1 must name its predecessor hash")]
97 MissingPredecessor,
98 #[error("Store acknowledgement sequence must start at 1, got {0}")]
99 InvalidAckSequence(u64),
100 #[error("Store acknowledgement sequence 1 must not name a predecessor object")]
101 UnexpectedAckPredecessor,
102 #[error("Store acknowledgement after sequence 1 must name its predecessor object")]
103 MissingAckPredecessor,
104 #[error(
105 "invalid membership coordinate {author}/{grant}/{stream_id}/{seq} with entry hash {entry_hash}"
106 )]
107 InvalidMembershipCoordinate {
108 author: String,
109 grant: String,
110 stream_id: String,
111 seq: u64,
112 entry_hash: String,
113 },
114 #[error("Store package length exceeds the platform address space")]
115 PackageTooLarge,
116 #[error("Store package length is {actual}, expected {expected}")]
117 PackageLengthMismatch { expected: u64, actual: u64 },
118 #[error("Store package hash is {actual}, expected {expected}")]
119 PackageHashMismatch {
120 expected: ObjectHash,
121 actual: ObjectHash,
122 },
123 #[error("Store object hash is {actual}, expected {expected}")]
124 ObjectHashMismatch {
125 expected: ObjectHash,
126 actual: ObjectHash,
127 },
128}
129
130#[derive(Clone, Debug)]
131#[doc(hidden)]
132pub struct VerifiedStoreBatchCommit {
133 reference: StoreBatchCommitRef,
134 value: std::sync::Arc<StoreBatchCommit>,
137 author: std::sync::Arc<StoreDeviceRegistration>,
138}
139
140fn parse_store_batch_commit(
141 bytes: &[u8],
142 expected_store_root_hash: ObjectHash,
143 expected_coord: &StoreCommitCoord,
144 author: &StoreDeviceRegistration,
145) -> Result<StoreBatchCommit, StoreProtocolError> {
146 let commit: StoreBatchCommit = crate::objects::decode_protocol_object(bytes)?;
147 commit.verify_at(expected_store_root_hash, expected_coord, author)?;
148 Ok(commit)
149}
150
151impl VerifiedStoreBatchCommit {
152 pub fn parse_prepared(
153 bytes: &[u8],
154 store_root_hash: ObjectHash,
155 coord: StoreCommitCoord,
156 object: ExactObjectRef,
157 author: &StoreDeviceRegistration,
158 ) -> Result<Self, StoreProtocolError> {
159 let value = parse_store_batch_commit(bytes, store_root_hash, &coord, author)?;
160 let reference = StoreBatchCommitRef::from_commit(&value, coord, object)?;
161 Ok(Self {
162 reference,
163 value: std::sync::Arc::new(value),
164 author: std::sync::Arc::new(author.clone()),
165 })
166 }
167
168 pub fn parse(
169 bytes: &[u8],
170 store_root_hash: ObjectHash,
171 reference: &StoreBatchCommitRef,
172 author: &StoreDeviceRegistration,
173 ) -> Result<Self, StoreProtocolError> {
174 let value = parse_store_batch_commit(bytes, store_root_hash, &reference.coord, author)?;
175 reference.verify_commit(&value)?;
176 Ok(Self {
177 reference: reference.clone(),
178 value: std::sync::Arc::new(value),
179 author: std::sync::Arc::new(author.clone()),
180 })
181 }
182
183 pub fn store_root_hash(&self) -> ObjectHash {
184 self.value.store_root_hash
185 }
186
187 pub fn reference(&self) -> &StoreBatchCommitRef {
188 &self.reference
189 }
190
191 pub fn value(&self) -> &StoreBatchCommit {
192 &self.value
193 }
194
195 pub fn author(&self) -> &StoreDeviceRegistration {
196 &self.author
197 }
198}
199
200impl std::ops::Deref for VerifiedStoreBatchCommit {
201 type Target = StoreBatchCommit;
202
203 fn deref(&self) -> &Self::Target {
204 self.value()
205 }
206}
207
208pub fn store_protocol_root_logical_key() -> &'static str {
209 STORE_PROTOCOL_ROOT_SEMANTIC_PATH
210}
211
212pub fn circle_access_leaf_semantic_prefix(
213 circle_id: CircleId,
214 family: CandidateFamilyId,
215 owner_pubkey: &str,
216 epoch_id: CircleEpochId,
217 recipient_slot: &str,
218 leaf_id: AccessLeafId,
219) -> String {
220 format!(
221 "circles/{circle_id}/candidates/{}/access-leaves/{owner_pubkey}/{epoch_id}/{recipient_slot}/{leaf_id}",
222 family.as_hash(),
223 )
224}
225
226pub fn circle_access_envelope_semantic_prefix(
227 circle_id: CircleId,
228 family: CandidateFamilyId,
229 owner_pubkey: &str,
230 recipient_slot: &str,
231 control_hash: ObjectHash,
232) -> String {
233 format!(
234 "circles/{circle_id}/candidates/{}/access-envelopes/{owner_pubkey}/{recipient_slot}/{control_hash}",
235 family.as_hash(),
236 )
237}
238
239pub fn device_join_abandonment_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
240 format!("{STORE_DEVICE_JOIN_ABANDONMENT_PREFIX}{attempt_id}")
241}
242
243pub fn device_join_cleanup_receipt_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
244 format!("{STORE_DEVICE_JOIN_CLEANUP_RECEIPT_PREFIX}{attempt_id}")
245}
246
247pub fn device_exclusion_proposal_semantic_prefix(
248 target: StoreDeviceId,
249 proposal_id: StoreDeviceExclusionProposalId,
250 proposal_hash: ObjectHash,
251) -> String {
252 format!("{STORE_DEVICE_EXCLUSION_PROPOSAL_PREFIX}{target}/{proposal_id}/{proposal_hash}")
253}
254
255pub fn device_exclusion_outcome_semantic_prefix(
256 target: StoreDeviceId,
257 proposal_id: StoreDeviceExclusionProposalId,
258) -> String {
259 format!("{STORE_DEVICE_EXCLUSION_OUTCOME_PREFIX}{target}/{proposal_id}")
260}
261
262pub fn provider_access_grant_semantic_prefix(
263 grant_id: &crate::provider::ProviderAccessGrantId,
264) -> String {
265 format!("{STORE_PROVIDER_ACCESS_GRANT_PREFIX}{}", grant_id.0)
266}
267
268pub fn owner_recovery_semantic_prefix(
269 owner_pubkey: &str,
270 owner_grant: MembershipGrantId,
271 sequence: u64,
272) -> String {
273 format!("{STORE_OWNER_RECOVERY_PREFIX}{owner_pubkey}/{owner_grant}/{sequence}")
274}
275
276pub fn package_semantic_prefix(
277 family: CandidateFamilyId,
278 device_id: &str,
279 seq: u64,
280 package_hash: ObjectHash,
281) -> String {
282 format!(
283 "{STORE_CANDIDATE_PREFIX}{}/packages/{device_id}/{seq}/{package_hash}",
284 family.as_hash()
285 )
286}
287
288pub fn circle_package_semantic_prefix(
289 circle_id: CircleId,
290 family: CandidateFamilyId,
291 device_id: &str,
292 seq: u64,
293 package_hash: ObjectHash,
294) -> String {
295 format!(
296 "circles/{circle_id}/candidates/{}/packages/{device_id}/{seq}/{package_hash}",
297 family.as_hash()
298 )
299}
300
301pub fn circle_bootstrap_image_semantic_prefix(
302 circle_id: CircleId,
303 family: CandidateFamilyId,
304 owner_pubkey: &str,
305 epoch_id: CircleEpochId,
306 recipient_slot: &str,
307 image_hash: ObjectHash,
308) -> String {
309 format!(
310 "circles/{circle_id}/candidates/{}/bootstraps/{owner_pubkey}/{epoch_id}/{recipient_slot}/{image_hash}",
311 family.as_hash(),
312 )
313}
314
315pub(crate) fn commit_slot_prefix(device_id: &str, seq: u64) -> String {
316 format!("{STORE_CANDIDATE_PREFIX}*/commits/{device_id}/{seq}")
317}
318
319pub fn commit_semantic_prefix(
320 family: CandidateFamilyId,
321 device_id: &str,
322 seq: u64,
323 commit_hash: ObjectHash,
324) -> String {
325 format!(
326 "{STORE_CANDIDATE_PREFIX}{}/commits/{device_id}/{seq}/{commit_hash}",
327 family.as_hash()
328 )
329}
330
331pub fn semantic_prefix_from_exact_object(
332 object: &ExactObjectRef,
333 extension: &str,
334) -> Result<String, StoreProtocolError> {
335 object
336 .slot()
337 .logical_key()
338 .strip_suffix(extension)
339 .map(str::to_string)
340 .ok_or_else(|| StoreProtocolError::RelocatedSlot {
341 expected: format!("candidate object ending in {extension}"),
342 actual: object.slot().logical_key().to_string(),
343 })
344}
345
346pub(crate) fn registration_slot_prefix(device_id: &str) -> String {
347 format!("{STORE_DEVICE_REGISTRATION_PREFIX}{device_id}")
348}
349
350pub fn registration_semantic_prefix(device_id: &str) -> String {
351 registration_slot_prefix(device_id)
352}
353
354pub fn founder_registration_semantic_prefix(creation_id: StoreCreationId) -> String {
355 format!("store-v1/devices/founder/{creation_id}/registration")
356}
357
358pub fn founder_membership_head_semantic_prefix(creation_id: StoreCreationId) -> String {
359 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}founder/{creation_id}/1")
360}
361
362pub fn ack_slot_prefix(device_id: &str, revision: u64) -> String {
363 format!("{STORE_ACK_PREFIX}{device_id}/{revision}")
364}
365
366pub fn circle_ack_slot_prefix(circle_id: CircleId, device_id: &str, sequence: u64) -> String {
367 format!("circles/{circle_id}/acks/{device_id}/{sequence}")
368}
369
370pub fn circle_snapshot_slot_prefix(
371 circle_id: CircleId,
372 device_id: &str,
373 generation: u64,
374) -> String {
375 format!("circles/{circle_id}/snapshots/{device_id}/{generation}")
376}
377
378pub fn circle_snapshot_image_semantic_prefix(
379 circle_id: CircleId,
380 device_id: &str,
381 image_hash: ObjectHash,
382) -> String {
383 format!("circles/{circle_id}/snapshot-images/{device_id}/{image_hash}")
384}
385
386pub fn snapshot_candidate_semantic_prefix(device_id: &str, candidate_id: &str) -> String {
388 format!("{STORE_SNAPSHOT_META_PREFIX}{device_id}/{candidate_id}")
389}
390
391pub fn membership_entry_semantic_prefix(
392 author: &str,
393 author_owner_grant: &MembershipGrantId,
394 stream_id: AuthorStreamId,
395 seq: u64,
396 entry_hash: ObjectHash,
397) -> String {
398 format!(
399 "{STORE_MEMBERSHIP_ENTRY_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{entry_hash}"
400 )
401}
402
403#[cfg(test)]
404pub(crate) fn membership_head_semantic_prefix(
405 author: &str,
406 author_owner_grant: &MembershipGrantId,
407 stream_id: AuthorStreamId,
408 seq: u64,
409 head_hash: ObjectHash,
410) -> String {
411 format!(
412 "{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{head_hash}"
413 )
414}
415
416pub fn membership_head_stream_prefix(
424 author: &str,
425 author_owner_grant: &MembershipGrantId,
426 stream_id: AuthorStreamId,
427) -> String {
428 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/")
429}
430
431pub fn membership_head_slot_prefix(
432 author: &str,
433 author_owner_grant: &MembershipGrantId,
434 stream_id: AuthorStreamId,
435 seq: u64,
436) -> String {
437 format!(
438 "{}{seq}",
439 membership_head_stream_prefix(author, author_owner_grant, stream_id)
440 )
441}
442
443pub fn membership_rollup_semantic_prefix(
444 metadata_slot: &ObjectSlot,
445 rollup_hash: ObjectHash,
446) -> String {
447 let owner = snapshot_artifact_owner(metadata_slot);
448 format!("{STORE_MEMBERSHIP_ROLLUP_PREFIX}{owner}/{rollup_hash}")
449}
450
451pub fn snapshot_image_semantic_prefix(
452 metadata_slot: &ObjectSlot,
453 image_hash: ObjectHash,
454) -> String {
455 let owner = snapshot_artifact_owner(metadata_slot);
456 format!("{STORE_SNAPSHOT_IMAGE_PREFIX}{owner}/{image_hash}")
457}
458
459fn snapshot_artifact_owner(metadata_slot: &ObjectSlot) -> ObjectHash {
460 ObjectHash::digest(&domain_json(
461 b"coven.store-snapshot-artifact-owner.v1\0",
462 metadata_slot,
463 ))
464}
465
466pub(crate) fn domain_json(domain: &[u8], value: &impl Serialize) -> Vec<u8> {
467 let json = serde_json::to_vec(value).expect("canonical Store fields serialize");
468 let mut bytes = Vec::with_capacity(domain.len() + json.len());
469 bytes.extend_from_slice(domain);
470 bytes.extend_from_slice(&json);
471 bytes
472}
473
474pub(crate) fn require_version(version: u32) -> Result<(), StoreProtocolError> {
475 if version == STORE_PROTOCOL_VERSION {
476 Ok(())
477 } else {
478 Err(StoreProtocolError::UnsupportedVersion(version))
479 }
480}
481
482pub(super) fn validate_commit_order(order: &StoreCommitOrder) -> Result<(), StoreProtocolError> {
483 let seq = order.seq();
484 if seq == 0 {
485 return Err(StoreProtocolError::InvalidSequence(0));
486 }
487 {
488 let predecessor = &order.predecessor;
489 let dependencies = &order.dependencies;
490 match (seq, predecessor) {
491 (1, None) => {}
492 (1, Some(_)) => return Err(StoreProtocolError::UnexpectedPredecessor),
493 (_, None) => return Err(StoreProtocolError::MissingPredecessor),
494 (_, Some(reference)) => {
495 if reference.coord.sequence.checked_add(1) != Some(seq) {
496 return Err(StoreProtocolError::Malformed(
497 "predecessor is not the preceding author-stream commit".to_string(),
498 ));
499 }
500 }
501 }
502 for (stream_id, reference) in dependencies {
503 if reference.coord.stream_id != *stream_id || reference.coord.sequence == 0 {
504 return Err(StoreProtocolError::Malformed(format!(
505 "dependency {stream_id} has a different exact coordinate"
506 )));
507 }
508 }
509 }
510 Ok(())
511}
512
513pub(super) fn validate_commit_predecessor_states(
514 order: &StoreCommitOrder,
515 membership: &StoreMembershipStateRef,
516 devices: &StoreDeviceStateRef,
517) -> Result<(), StoreProtocolError> {
518 membership.validate_shape()?;
519 if membership.recovery() != devices.recovery() {
520 return Err(StoreProtocolError::OwnerRecoveryMismatch);
521 }
522 let expected = order.predecessor_cut()?;
523 if devices.frontier().commits() != expected.commits() {
524 return Err(StoreProtocolError::Malformed(
525 "Store device state names a different Merge predecessor cut".to_string(),
526 ));
527 }
528 Ok(())
529}
530
531pub(crate) fn validate_commit_frontier(
532 frontier: &CommitFrontier,
533) -> Result<(), StoreProtocolError> {
534 {
535 for (stream_id, reference) in &frontier.0 {
536 if reference.coord.stream_id != *stream_id || reference.coord.sequence == 0 {
537 return Err(StoreProtocolError::Malformed(format!(
538 "frontier entry {stream_id} has a different exact coordinate"
539 )));
540 }
541 }
542 Ok(())
543 }
544}
545
546pub(crate) fn validate_store_history_cut(
547 frontier: &StoreHistoryCut,
548) -> Result<(), StoreProtocolError> {
549 validate_commit_frontier(&CommitFrontier(frontier.0.clone()))
550}
551
552pub(super) fn validate_successor_sequence(
553 sequence: u64,
554 successor: &SuccessorLink,
555) -> Result<(), StoreProtocolError> {
556 match (sequence, successor.predecessor.is_some()) {
557 (0, _) => Err(StoreProtocolError::InvalidAckSequence(0)),
558 (1, false) => Ok(()),
559 (1, true) => Err(StoreProtocolError::UnexpectedAckPredecessor),
560 (_, true) => Ok(()),
561 (_, false) => Err(StoreProtocolError::MissingAckPredecessor),
562 }
563}
564
565pub(super) fn validate_ack_state(
566 store_cut: &StoreHistoryCut,
567 device_state: &StoreDeviceStateRef,
568) -> Result<(), StoreProtocolError> {
569 validate_store_history_cut(store_cut)?;
570 if device_state.frontier() != &store_cut.frontier() {
571 return Err(StoreProtocolError::DeviceStateMismatch);
572 }
573 Ok(())
574}
575
576pub(super) fn validate_membership_coord(coord: &MembershipCoord) -> Result<(), StoreProtocolError> {
577 if coord.seq == 0 || coord.author_pubkey.is_empty() {
578 return Err(StoreProtocolError::InvalidMembershipCoordinate {
579 author: coord.author_pubkey.clone(),
580 grant: coord.author_owner_grant.to_string(),
581 stream_id: coord.stream_id.to_string(),
582 seq: coord.seq,
583 entry_hash: coord.entry_hash.to_string(),
584 });
585 }
586 Ok(())
587}
588
589impl From<coven_foundation::object_hash::InvalidObjectHash> for StoreProtocolError {
590 fn from(error: coven_foundation::object_hash::InvalidObjectHash) -> Self {
591 StoreProtocolError::InvalidObjectHash(error.0)
592 }
593}
594
595impl From<crate::provider::ProviderProbeError> for StoreProtocolError {
596 fn from(error: crate::provider::ProviderProbeError) -> Self {
597 Self::ProviderProbe(Box::new(error))
598 }
599}