Skip to main content

coven_protocol/store_commit/
validation.rs

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
422/// Everything one author stream's head slots share, up to the sequence number.
423///
424/// A head's slot is named by its coordinate alone, so a provider can enumerate
425/// a whole stream under this prefix instead of a reader learning each slot from
426/// the head before it. The founder's first head is the exception: it is written
427/// under [`founder_membership_head_semantic_prefix`] before the founder has a
428/// grant to name, so a listing here starts the founder's stream at sequence 2.
429pub 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}