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("invalid Store membership resolution authority for resolver {0:?}")]
115 InvalidMembershipResolutionAuthority(String),
116 #[error("Store package length exceeds the platform address space")]
117 PackageTooLarge,
118 #[error("Store package length is {actual}, expected {expected}")]
119 PackageLengthMismatch { expected: u64, actual: u64 },
120 #[error("Store package hash is {actual}, expected {expected}")]
121 PackageHashMismatch {
122 expected: ObjectHash,
123 actual: ObjectHash,
124 },
125 #[error("Store object hash is {actual}, expected {expected}")]
126 ObjectHashMismatch {
127 expected: ObjectHash,
128 actual: ObjectHash,
129 },
130}
131
132#[derive(Clone, Debug)]
133#[doc(hidden)]
134pub struct VerifiedStoreBatchCommit {
135 store_root_hash: ObjectHash,
136 reference: StoreBatchCommitRef,
137 value: StoreBatchCommit,
138 author: StoreDeviceRegistration,
139}
140
141fn parse_store_batch_commit(
142 bytes: &[u8],
143 expected_store_root_hash: ObjectHash,
144 expected_coord: &StoreCommitCoord,
145 author: &StoreDeviceRegistration,
146) -> Result<StoreBatchCommit, StoreProtocolError> {
147 let commit: StoreBatchCommit = crate::objects::decode_protocol_object(bytes)?;
148 commit.verify_at(expected_store_root_hash, expected_coord, author)?;
149 Ok(commit)
150}
151
152impl VerifiedStoreBatchCommit {
153 pub fn parse_prepared(
154 bytes: &[u8],
155 store_root_hash: ObjectHash,
156 coord: StoreCommitCoord,
157 object: ExactObjectRef,
158 author: &StoreDeviceRegistration,
159 ) -> Result<Self, StoreProtocolError> {
160 let value = parse_store_batch_commit(bytes, store_root_hash, &coord, author)?;
161 let reference = StoreBatchCommitRef::from_commit(&value, coord, object)?;
162 Ok(Self {
163 store_root_hash,
164 reference,
165 value,
166 author: author.clone(),
167 })
168 }
169
170 pub fn parse(
171 bytes: &[u8],
172 store_root_hash: ObjectHash,
173 reference: &StoreBatchCommitRef,
174 author: &StoreDeviceRegistration,
175 ) -> Result<Self, StoreProtocolError> {
176 let value = parse_store_batch_commit(bytes, store_root_hash, &reference.coord, author)?;
177 reference.verify_commit(&value)?;
178 Ok(Self {
179 store_root_hash,
180 reference: reference.clone(),
181 value,
182 author: author.clone(),
183 })
184 }
185
186 pub fn store_root_hash(&self) -> ObjectHash {
187 self.store_root_hash
188 }
189
190 pub fn reference(&self) -> &StoreBatchCommitRef {
191 &self.reference
192 }
193
194 pub fn value(&self) -> &StoreBatchCommit {
195 &self.value
196 }
197
198 pub fn author(&self) -> &StoreDeviceRegistration {
199 &self.author
200 }
201}
202
203impl std::ops::Deref for VerifiedStoreBatchCommit {
204 type Target = StoreBatchCommit;
205
206 fn deref(&self) -> &Self::Target {
207 self.value()
208 }
209}
210
211pub fn store_protocol_root_logical_key() -> &'static str {
212 STORE_PROTOCOL_ROOT_SEMANTIC_PATH
213}
214
215pub fn circle_access_leaf_semantic_prefix(
216 circle_id: CircleId,
217 family: CandidateFamilyId,
218 owner_pubkey: &str,
219 epoch_id: CircleEpochId,
220 recipient_slot: &str,
221 leaf_id: AccessLeafId,
222) -> String {
223 format!(
224 "circles/{circle_id}/candidates/{}/access-leaves/{owner_pubkey}/{epoch_id}/{recipient_slot}/{leaf_id}",
225 family.as_hash(),
226 )
227}
228
229pub fn circle_access_envelope_semantic_prefix(
230 circle_id: CircleId,
231 family: CandidateFamilyId,
232 owner_pubkey: &str,
233 recipient_slot: &str,
234 control_hash: ObjectHash,
235) -> String {
236 format!(
237 "circles/{circle_id}/candidates/{}/access-envelopes/{owner_pubkey}/{recipient_slot}/{control_hash}",
238 family.as_hash(),
239 )
240}
241
242pub fn device_join_abandonment_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
243 format!("{STORE_DEVICE_JOIN_ABANDONMENT_PREFIX}{attempt_id}")
244}
245
246pub fn device_join_cleanup_receipt_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
247 format!("{STORE_DEVICE_JOIN_CLEANUP_RECEIPT_PREFIX}{attempt_id}")
248}
249
250pub fn device_exclusion_proposal_semantic_prefix(
251 target: StoreDeviceId,
252 proposal_id: StoreDeviceExclusionProposalId,
253 proposal_hash: ObjectHash,
254) -> String {
255 format!("{STORE_DEVICE_EXCLUSION_PROPOSAL_PREFIX}{target}/{proposal_id}/{proposal_hash}")
256}
257
258pub fn device_exclusion_outcome_semantic_prefix(
259 target: StoreDeviceId,
260 proposal_id: StoreDeviceExclusionProposalId,
261) -> String {
262 format!("{STORE_DEVICE_EXCLUSION_OUTCOME_PREFIX}{target}/{proposal_id}")
263}
264
265pub fn provider_access_grant_semantic_prefix(
266 grant_id: &crate::provider::ProviderAccessGrantId,
267) -> String {
268 format!("{STORE_PROVIDER_ACCESS_GRANT_PREFIX}{}", grant_id.0)
269}
270
271pub fn owner_recovery_semantic_prefix(
272 owner_pubkey: &str,
273 owner_grant: MembershipGrantId,
274 sequence: u64,
275) -> String {
276 format!("{STORE_OWNER_RECOVERY_PREFIX}{owner_pubkey}/{owner_grant}/{sequence}")
277}
278
279pub fn package_semantic_prefix(
280 family: CandidateFamilyId,
281 device_id: &str,
282 seq: u64,
283 package_hash: ObjectHash,
284) -> String {
285 format!(
286 "{STORE_CANDIDATE_PREFIX}{}/packages/{device_id}/{seq}/{package_hash}",
287 family.as_hash()
288 )
289}
290
291pub fn circle_package_semantic_prefix(
292 circle_id: CircleId,
293 family: CandidateFamilyId,
294 device_id: &str,
295 seq: u64,
296 package_hash: ObjectHash,
297) -> String {
298 format!(
299 "circles/{circle_id}/candidates/{}/packages/{device_id}/{seq}/{package_hash}",
300 family.as_hash()
301 )
302}
303
304pub fn circle_bootstrap_image_semantic_prefix(
305 circle_id: CircleId,
306 family: CandidateFamilyId,
307 owner_pubkey: &str,
308 epoch_id: CircleEpochId,
309 recipient_slot: &str,
310 image_hash: ObjectHash,
311) -> String {
312 format!(
313 "circles/{circle_id}/candidates/{}/bootstraps/{owner_pubkey}/{epoch_id}/{recipient_slot}/{image_hash}",
314 family.as_hash(),
315 )
316}
317
318pub(crate) fn commit_slot_prefix(device_id: &str, seq: u64) -> String {
319 format!("{STORE_CANDIDATE_PREFIX}*/commits/{device_id}/{seq}")
320}
321
322pub fn commit_semantic_prefix(
323 family: CandidateFamilyId,
324 device_id: &str,
325 seq: u64,
326 commit_hash: ObjectHash,
327) -> String {
328 format!(
329 "{STORE_CANDIDATE_PREFIX}{}/commits/{device_id}/{seq}/{commit_hash}",
330 family.as_hash()
331 )
332}
333
334pub fn semantic_prefix_from_exact_object(
335 object: &ExactObjectRef,
336 extension: &str,
337) -> Result<String, StoreProtocolError> {
338 object
339 .slot()
340 .logical_key()
341 .strip_suffix(extension)
342 .map(str::to_string)
343 .ok_or_else(|| StoreProtocolError::RelocatedSlot {
344 expected: format!("candidate object ending in {extension}"),
345 actual: object.slot().logical_key().to_string(),
346 })
347}
348
349pub fn head_slot_prefix(device_id: &str, seq: u64) -> String {
350 format!("{STORE_HEAD_PREFIX}{device_id}/{seq}")
351}
352
353pub(crate) fn registration_slot_prefix(device_id: &str) -> String {
354 format!("{STORE_DEVICE_REGISTRATION_PREFIX}{device_id}")
355}
356
357pub fn registration_semantic_prefix(device_id: &str) -> String {
358 registration_slot_prefix(device_id)
359}
360
361pub fn founder_registration_semantic_prefix(creation_id: StoreCreationId) -> String {
362 format!("store-v1/devices/founder/{creation_id}/registration")
363}
364
365pub fn founder_membership_head_semantic_prefix(creation_id: StoreCreationId) -> String {
366 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}founder/{creation_id}/1")
367}
368
369pub fn ack_slot_prefix(device_id: &str, revision: u64) -> String {
370 format!("{STORE_ACK_PREFIX}{device_id}/{revision}")
371}
372
373pub fn circle_ack_slot_prefix(circle_id: CircleId, device_id: &str, sequence: u64) -> String {
374 format!("circles/{circle_id}/acks/{device_id}/{sequence}")
375}
376
377pub fn circle_snapshot_slot_prefix(
378 circle_id: CircleId,
379 device_id: &str,
380 generation: u64,
381) -> String {
382 format!("circles/{circle_id}/snapshots/{device_id}/{generation}")
383}
384
385pub fn circle_snapshot_image_semantic_prefix(
386 circle_id: CircleId,
387 device_id: &str,
388 image_hash: ObjectHash,
389) -> String {
390 format!("circles/{circle_id}/snapshot-images/{device_id}/{image_hash}")
391}
392
393pub fn snapshot_slot_prefix(device_id: &str, generation: u64) -> String {
394 format!("{STORE_SNAPSHOT_META_PREFIX}{device_id}/{generation}")
395}
396
397pub fn membership_entry_semantic_prefix(
398 author: &str,
399 author_owner_grant: &MembershipGrantId,
400 stream_id: AuthorStreamId,
401 seq: u64,
402 entry_hash: ObjectHash,
403) -> String {
404 format!(
405 "{STORE_MEMBERSHIP_ENTRY_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{entry_hash}"
406 )
407}
408
409#[cfg(test)]
410pub(crate) fn membership_head_semantic_prefix(
411 author: &str,
412 author_owner_grant: &MembershipGrantId,
413 stream_id: AuthorStreamId,
414 seq: u64,
415 head_hash: ObjectHash,
416) -> String {
417 format!(
418 "{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{head_hash}"
419 )
420}
421
422pub fn membership_head_stream_prefix(
430 author: &str,
431 author_owner_grant: &MembershipGrantId,
432 stream_id: AuthorStreamId,
433) -> String {
434 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/")
435}
436
437pub fn membership_head_slot_prefix(
438 author: &str,
439 author_owner_grant: &MembershipGrantId,
440 stream_id: AuthorStreamId,
441 seq: u64,
442) -> String {
443 format!(
444 "{}{seq}",
445 membership_head_stream_prefix(author, author_owner_grant, stream_id)
446 )
447}
448
449pub fn membership_resolution_semantic_prefix(
450 conflict_hash: ObjectHash,
451 resolver: &str,
452 resolution_hash: ObjectHash,
453) -> String {
454 format!("store-v1/membership/resolutions/{conflict_hash}/{resolver}/{resolution_hash}")
455}
456
457pub fn membership_rollup_semantic_prefix(author: &str, rollup_hash: ObjectHash) -> String {
458 format!("{STORE_MEMBERSHIP_ROLLUP_PREFIX}{author}/{rollup_hash}")
459}
460
461pub fn snapshot_image_semantic_prefix(author: &str, image_hash: ObjectHash) -> String {
462 format!("{STORE_SNAPSHOT_IMAGE_PREFIX}{author}/{image_hash}")
463}
464
465pub(crate) fn snapshot_semantic_prefix(author: &str, snapshot_hash: ObjectHash) -> String {
466 format!("{STORE_SNAPSHOT_META_PREFIX}{author}/{snapshot_hash}")
467}
468
469pub(crate) fn domain_json(domain: &[u8], value: &impl Serialize) -> Vec<u8> {
470 let json = serde_json::to_vec(value).expect("canonical Store fields serialize");
471 let mut bytes = Vec::with_capacity(domain.len() + json.len());
472 bytes.extend_from_slice(domain);
473 bytes.extend_from_slice(&json);
474 bytes
475}
476
477pub(crate) fn require_version(version: u32) -> Result<(), StoreProtocolError> {
478 if version == STORE_PROTOCOL_VERSION {
479 Ok(())
480 } else {
481 Err(StoreProtocolError::UnsupportedVersion(version))
482 }
483}
484
485pub(super) fn validate_commit_order(order: &StoreCommitOrder) -> Result<(), StoreProtocolError> {
486 let seq = order.seq();
487 if seq == 0 {
488 return Err(StoreProtocolError::InvalidSequence(0));
489 }
490 {
491 let predecessor = &order.predecessor;
492 let dependencies = &order.dependencies;
493 match (seq, predecessor) {
494 (1, None) => {}
495 (1, Some(_)) => return Err(StoreProtocolError::UnexpectedPredecessor),
496 (_, None) => return Err(StoreProtocolError::MissingPredecessor),
497 (_, Some(reference)) => {
498 if reference.coord.sequence.checked_add(1) != Some(seq) {
499 return Err(StoreProtocolError::Malformed(
500 "predecessor is not the preceding author-stream commit".to_string(),
501 ));
502 }
503 }
504 }
505 for (stream_id, reference) in dependencies {
506 if reference.coord.stream_id != *stream_id || reference.coord.sequence == 0 {
507 return Err(StoreProtocolError::Malformed(format!(
508 "dependency {stream_id} has a different exact coordinate"
509 )));
510 }
511 }
512 }
513 Ok(())
514}
515
516pub(super) fn validate_commit_predecessor_states(
517 order: &StoreCommitOrder,
518 membership: &StoreMembershipStateRef,
519 devices: &StoreDeviceStateRef,
520) -> Result<(), StoreProtocolError> {
521 membership.validate_shape()?;
522 if membership.recovery() != devices.recovery() {
523 return Err(StoreProtocolError::OwnerRecoveryMismatch);
524 }
525 validate_recovery_cursors(membership.recovery())?;
526 validate_recovery_cursors(devices.recovery())?;
527 {
528 let mut expected = order.dependencies.clone();
529 if let Some(predecessor) = &order.predecessor {
530 if expected
531 .insert(predecessor.coord.stream_id, predecessor.clone())
532 .is_some_and(|dependency| dependency != *predecessor)
533 {
534 return Err(StoreProtocolError::Malformed(
535 "Merge predecessor disagrees with the same-stream dependency".to_string(),
536 ));
537 }
538 }
539 if devices.frontier() != &CommitFrontier(expected) {
540 return Err(StoreProtocolError::Malformed(
541 "Store device state names a different Merge predecessor cut".to_string(),
542 ));
543 }
544 Ok(())
545 }
546}
547
548pub(crate) fn validate_commit_frontier(
549 frontier: &CommitFrontier,
550) -> Result<(), StoreProtocolError> {
551 {
552 for (stream_id, reference) in &frontier.0 {
553 if reference.coord.stream_id != *stream_id || reference.coord.sequence == 0 {
554 return Err(StoreProtocolError::Malformed(format!(
555 "frontier entry {stream_id} has a different exact coordinate"
556 )));
557 }
558 }
559 Ok(())
560 }
561}
562
563pub(crate) fn validate_store_history_cut(
564 frontier: &StoreHistoryCut,
565) -> Result<(), StoreProtocolError> {
566 validate_commit_frontier(&CommitFrontier(frontier.0.clone()))
567}
568
569pub(super) fn validate_store_device_state_ref(
570 state: &StoreDeviceStateRef,
571) -> Result<(), StoreProtocolError> {
572 validate_recovery_cursors(state.recovery())?;
573 validate_commit_frontier(state.frontier())
574}
575
576pub(super) fn validate_successor_sequence(
577 sequence: u64,
578 successor: &SuccessorLink,
579) -> Result<(), StoreProtocolError> {
580 match (sequence, successor.predecessor.is_some()) {
581 (0, _) => Err(StoreProtocolError::InvalidAckSequence(0)),
582 (1, false) => Ok(()),
583 (1, true) => Err(StoreProtocolError::UnexpectedAckPredecessor),
584 (_, true) => Ok(()),
585 (_, false) => Err(StoreProtocolError::MissingAckPredecessor),
586 }
587}
588
589pub(super) fn validate_ack_state(
590 store_root_hash: ObjectHash,
591 registration: &StoreDeviceRegistrationRef,
592 store_cut: &StoreHistoryCut,
593 device_state: &StoreDeviceStateRef,
594 exclusions: &StoreAckExclusionState,
595) -> Result<(), StoreProtocolError> {
596 validate_store_history_cut(store_cut)?;
597 let _ = (store_root_hash, registration);
598 let state_matches = device_state.frontier() == &store_cut.frontier();
599 if !state_matches {
600 return Err(StoreProtocolError::DeviceStateMismatch);
601 }
602 {
603 let proposal_freezes = &exclusions.proposal_freezes;
604 if proposal_freezes
605 .windows(2)
606 .any(|pair| pair[0].proposal.proposal_id >= pair[1].proposal.proposal_id)
607 {
608 return Err(StoreProtocolError::DeviceStateMismatch);
609 }
610 for freeze in proposal_freezes {
611 validate_store_history_cut(&freeze.target_cut)?;
612 freeze.proposal.validate_path()?;
613 if !store_cut.frontier().covers(&freeze.target_cut.frontier()) {
614 return Err(StoreProtocolError::DeviceStateMismatch);
615 }
616 }
617 Ok(())
618 }
619}
620
621fn validate_membership_coord(coord: &MembershipCoord) -> Result<(), StoreProtocolError> {
622 if coord.seq == 0 || coord.author_pubkey.is_empty() {
623 return Err(StoreProtocolError::InvalidMembershipCoordinate {
624 author: coord.author_pubkey.clone(),
625 grant: coord.author_owner_grant.to_string(),
626 stream_id: coord.stream_id.to_string(),
627 seq: coord.seq,
628 entry_hash: coord.entry_hash.to_string(),
629 });
630 }
631 Ok(())
632}
633
634pub(super) fn validate_membership_authority(
635 authority: &MembershipGrantCreationAuthority,
636) -> Result<(), StoreProtocolError> {
637 match authority {
638 MembershipGrantCreationAuthority::Entry(coord) => validate_membership_coord(coord),
639 MembershipGrantCreationAuthority::ConflictResolution(reference) => {
640 let resolver = hex::decode(&reference.resolver_pubkey).map_err(|_| {
641 StoreProtocolError::InvalidMembershipResolutionAuthority(
642 reference.resolver_pubkey.clone(),
643 )
644 })?;
645 if resolver.len() != coven_keys::keys::SIGN_PUBLICKEYBYTES {
646 return Err(StoreProtocolError::InvalidMembershipResolutionAuthority(
647 reference.resolver_pubkey.clone(),
648 ));
649 }
650 Ok(())
651 }
652 }
653}
654
655pub(super) fn validate_operation_membership_authority(
656 authority: &MembershipGrantCreationAuthority,
657) -> Result<(), StoreProtocolError> {
658 validate_membership_authority(authority)
659}
660
661impl From<coven_foundation::object_hash::InvalidObjectHash> for StoreProtocolError {
662 fn from(error: coven_foundation::object_hash::InvalidObjectHash) -> Self {
663 StoreProtocolError::InvalidObjectHash(error.0)
664 }
665}
666
667impl From<crate::provider::ProviderProbeError> for StoreProtocolError {
668 fn from(error: crate::provider::ProviderProbeError) -> Self {
669 Self::ProviderProbe(Box::new(error))
670 }
671}