1use super::*;
2
3#[allow(clippy::too_many_arguments)]
4pub(super) fn validate_commit_envelope(
5 store_root_hash: ObjectHash,
6 coord: &StoreCommitCoord,
7 author_registration: &StoreDeviceRegistrationRef,
8 author: &StoreDeviceRegistration,
9 order: &StoreCommitOrder,
10 membership_state: &StoreMembershipStateRef,
11 device_state: &StoreDeviceStateRef,
12 membership_authority: Option<&MembershipGrantCreationAuthority>,
13 signer: &UserKeypair,
14) -> Result<(), StoreProtocolError> {
15 author_registration.verify_registration(author)?;
16 if keys::public_key_hex(signer) != author.device_signing_pubkey {
17 return Err(StoreProtocolError::InvalidSignature);
18 }
19 if order.seq() == 0 {
20 return Err(StoreProtocolError::InvalidSequence(0));
21 }
22 validate_commit_order(order)?;
23 validate_commit_predecessor_states(order, membership_state, device_state)?;
24 if coord.sequence() != order.seq() {
25 return Err(StoreProtocolError::Malformed(
26 "Store commit coordinate disagrees with its order".to_string(),
27 ));
28 }
29 if let Some(authority) = membership_authority {
30 validate_membership_authority(authority)?;
31 }
32 crate::objects::verify_store_root(store_root_hash, author.store_root.store_root_hash)?;
33 Ok(())
34}
35
36pub(super) fn validate_commit_body(
37 store_root_hash: ObjectHash,
38 body: &StoreCommitBody,
39 author: &StoreDeviceRegistrationRef,
40) -> Result<(), StoreProtocolError> {
41 match body {
42 StoreCommitBody::Operations(operations) => {
43 if operations.is_empty() {
44 return Err(StoreProtocolError::EmptyBatch);
45 }
46 validate_circle_control_refs(&operations.circle_controls)?;
47 validate_commit_acknowledgement(&operations.acknowledgement, author)?;
48 validate_commit_circle_acknowledgements(&operations.circle_acknowledgements, author)?;
49 validate_device_join_attempt_decision_refs(&operations.device_join_attempt_decisions)?;
50 validate_provider_access_refs(&operations.provider_access_grants)?;
51 validate_device_registration_refs(&operations.device_registrations)?;
52 validate_device_exclusion_refs(
53 &operations.device_exclusion_proposals,
54 &operations.device_exclusion_outcomes,
55 )?;
56 validate_stream_activations(
57 store_root_hash,
58 author,
59 operations.control.as_ref(),
60 &operations.stream_activations,
61 )?;
62 }
63 StoreCommitBody::ReclaimAuthorization { .. } => {}
64 StoreCommitBody::ReclaimReceipt { .. } => {}
65 StoreCommitBody::OwnerPromotionRequest { request } => {
66 if request.store_root_hash != store_root_hash
67 || request.promoter_registration != *author
68 {
69 return Err(StoreProtocolError::OwnerPromotionMismatch);
70 }
71 }
72 StoreCommitBody::AbandonCandidates { manifests } => {
73 if manifests.is_empty() {
74 return Err(StoreProtocolError::Malformed(
75 "candidate abandonment has no candidates".to_string(),
76 ));
77 }
78 }
79 }
80 Ok(())
81}
82
83pub(super) fn validate_owner_promotion_request_for_commit(
84 request: &OwnerPromotionRequest,
85 store_root_hash: ObjectHash,
86 author_registration: &StoreDeviceRegistrationRef,
87 author: &StoreDeviceRegistration,
88 membership_state: &StoreMembershipStateRef,
89 device_state: &StoreDeviceStateRef,
90) -> Result<(), StoreProtocolError> {
91 request.verify(&author.store_root, author)?;
92 if request.store_root_hash != store_root_hash
93 || request.promoter_registration != *author_registration
94 || request.predecessor_membership != *membership_state
95 || request.predecessor_devices != *device_state
96 {
97 return Err(StoreProtocolError::OwnerPromotionMismatch);
98 }
99 Ok(())
100}
101
102pub(crate) fn validate_stream_activations(
103 store_root_hash: ObjectHash,
104 author: &StoreDeviceRegistrationRef,
105 control: Option<&StoreControl>,
106 activations: &[StreamActivation],
107) -> Result<(), StoreProtocolError> {
108 if activations.windows(2).any(|pair| pair[0] >= pair[1]) {
109 return Err(StoreProtocolError::Malformed(
110 "stream activations are not strictly sorted and unique".to_string(),
111 ));
112 }
113 let mut activation_ids = BTreeSet::new();
114 let mut stream_ids = BTreeSet::new();
115 let mut first_slots = BTreeSet::new();
116 for activation in activations {
117 crate::objects::verify_store_root(store_root_hash, activation.store_root_hash())?;
118 let owner_promotion = control.is_some();
119 if activation.author_registration() != author && !owner_promotion {
120 return Err(StoreProtocolError::Malformed(
121 "stream activation registration differs from its commit author".to_string(),
122 ));
123 }
124 let allowed_anchor = matches!(
125 (control, activation),
126 (
127 Some(StoreControl { .. }),
128 StreamActivation::GrantAuthorized {
129 anchor: GrantStreamAnchor::StoreMembership { .. }
130 | GrantStreamAnchor::OwnerRecovery { .. },
131 ..
132 }
133 ) | (
134 _,
135 StreamActivation::GrantAuthorized {
136 anchor: GrantStreamAnchor::CircleControl { .. }
137 | GrantStreamAnchor::CircleRoster { .. }
138 | GrantStreamAnchor::CircleMetadata { .. },
139 ..
140 }
141 )
142 );
143 if !allowed_anchor {
144 return Err(StoreProtocolError::Malformed(
145 "Store commit contains a root- or registration-authorized stream anchor"
146 .to_string(),
147 ));
148 }
149 if !activation_ids.insert(activation.activation_id())
150 || !stream_ids.insert(activation.author_stream_id())
151 || !first_slots.insert(activation.first_slot().clone())
152 {
153 return Err(StoreProtocolError::Malformed(
154 "stream activations repeat an activation, author stream, or first slot".to_string(),
155 ));
156 }
157 }
158 Ok(())
159}
160
161pub(super) fn validate_candidate_abandonment(
162 manifests: &[CandidateCleanupManifest],
163 store_root_hash: ObjectHash,
164 author_registration: &StoreDeviceRegistrationRef,
165 coord: &StoreCommitCoord,
166 order: &StoreCommitOrder,
167 author: &StoreDeviceRegistration,
168) -> Result<(), StoreProtocolError> {
169 if manifests.is_empty() {
170 return Err(StoreProtocolError::Malformed(
171 "candidate abandonment has no candidates".to_string(),
172 ));
173 }
174 if manifests.windows(2).any(|pair| pair[0] >= pair[1]) {
175 return Err(StoreProtocolError::Malformed(
176 "candidate abandonment manifests are not strictly sorted and unique".to_string(),
177 ));
178 }
179 for manifest in manifests {
180 if &manifest.candidate.coord != coord {
181 return Err(StoreProtocolError::Malformed(
182 "abandoned candidate occupies a different competition point".to_string(),
183 ));
184 }
185 let candidate = manifest
186 .candidate
187 .verify_candidate(store_root_hash, author)?;
188 if &candidate.author_registration != author_registration {
189 return Err(StoreProtocolError::Malformed(
190 "abandoned candidate has a different author registration".to_string(),
191 ));
192 }
193 let shares_predecessor = candidate.order.predecessor == order.predecessor;
194 if !shares_predecessor {
195 return Err(StoreProtocolError::Malformed(
196 "abandoned candidate has a different predecessor".to_string(),
197 ));
198 }
199 }
200 Ok(())
201}
202
203pub(crate) fn candidate_manifest(
204 family: CandidateFamilyId,
205 body: &StoreCommitBody,
206) -> Result<CandidateObjectManifest, StoreProtocolError> {
207 let mut objects = Vec::new();
208 match body {
209 StoreCommitBody::Operations(operations) => {
210 objects.extend(
211 operations
212 .store_package
213 .iter()
214 .cloned()
215 .map(CandidateExclusiveObjectRef::StorePackage),
216 );
217 objects.extend(
218 operations
219 .circle_packages
220 .iter()
221 .cloned()
222 .map(CandidateExclusiveObjectRef::CirclePackage),
223 );
224 for control in &operations.circle_controls {
225 let circle_id = control.circle_id();
226 if let Some(reference) = &control.objects().close_intent {
227 objects.push(CandidateExclusiveObjectRef::CircleEpochCloseIntent {
228 circle_id,
229 reference: reference.clone(),
230 });
231 }
232 if let Some(reference) = &control.objects().close_outcome {
233 objects.push(CandidateExclusiveObjectRef::CircleEpochCloseOutcome {
234 circle_id,
235 reference: reference.clone(),
236 });
237 }
238 if let Some(reference) = &control.objects().close_cancellation {
239 objects.push(CandidateExclusiveObjectRef::CircleEpochCloseCancellation {
240 circle_id,
241 reference: reference.clone(),
242 });
243 }
244 if control
245 .objects()
246 .access
247 .iter()
248 .any(|access| access.envelope.control_hash != control.control().control_hash())
249 {
250 return Err(StoreProtocolError::Malformed(
251 "Circle access envelope differs from its activating control".to_string(),
252 ));
253 }
254 objects.extend(
255 control.objects().access.iter().cloned().map(|access| {
256 CandidateExclusiveObjectRef::CircleAccess { circle_id, access }
257 }),
258 );
259 }
260 }
261 StoreCommitBody::ReclaimAuthorization { .. } => {}
262 StoreCommitBody::ReclaimReceipt { .. } => {}
263 StoreCommitBody::OwnerPromotionRequest { .. }
264 | StoreCommitBody::AbandonCandidates { .. } => {}
265 }
266 objects.sort_by_cached_key(|object| {
267 serde_json::to_vec(object).expect("candidate object serialization cannot fail")
268 });
269 let mut exact_refs = BTreeSet::new();
270 let mut access_keys = BTreeSet::new();
271 for object in &objects {
272 validate_candidate_object_path(family, object)?;
273 match object {
274 CandidateExclusiveObjectRef::CircleAccess { circle_id, access } => {
275 let key = (
276 *circle_id,
277 access.leaf.owner_pubkey.clone(),
278 access.leaf.recipient_slot.clone(),
279 access.envelope.control_hash,
280 );
281 if !access_keys.insert(key) {
282 return Err(StoreProtocolError::Malformed(
283 "candidate object manifest repeats a Circle access semantic key"
284 .to_string(),
285 ));
286 }
287 insert_candidate_exact_ref(&mut exact_refs, &access.leaf.object)?;
288 insert_candidate_exact_ref(&mut exact_refs, &access.envelope.object)?;
289 }
290 CandidateExclusiveObjectRef::CircleEpochCloseIntent { reference, .. } => {
291 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
292 }
293 CandidateExclusiveObjectRef::CircleEpochCloseOutcome { reference, .. } => {
294 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
295 }
296 CandidateExclusiveObjectRef::CircleEpochCloseCancellation { reference, .. } => {
297 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
298 }
299 CandidateExclusiveObjectRef::StorePackage(reference) => {
300 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
301 }
302 CandidateExclusiveObjectRef::CirclePackage(reference) => {
303 insert_candidate_exact_ref(&mut exact_refs, &reference.package.object)?;
304 }
305 }
306 }
307 Ok(CandidateObjectManifest { family, objects })
308}
309
310pub(super) fn insert_candidate_exact_ref<'a>(
311 exact_refs: &mut BTreeSet<&'a ExactObjectRef>,
312 object: &'a ExactObjectRef,
313) -> Result<(), StoreProtocolError> {
314 if !exact_refs.insert(object) {
315 return Err(StoreProtocolError::Malformed(
316 "candidate object manifest repeats an exact object reference".to_string(),
317 ));
318 }
319 Ok(())
320}
321
322pub(super) fn validate_candidate_object_path(
323 family: CandidateFamilyId,
324 candidate: &CandidateExclusiveObjectRef,
325) -> Result<(), StoreProtocolError> {
326 match candidate {
327 CandidateExclusiveObjectRef::StorePackage(reference) => {
328 if reference.candidate_family != family {
329 return Err(StoreProtocolError::Malformed(
330 "Store package candidate family differs from its manifest".to_string(),
331 ));
332 }
333 Ok(())
334 }
335 CandidateExclusiveObjectRef::CirclePackage(reference) => {
336 if reference.package.candidate_family != family {
337 return Err(StoreProtocolError::Malformed(
338 "Circle package candidate family differs from its manifest".to_string(),
339 ));
340 }
341 Ok(())
342 }
343 CandidateExclusiveObjectRef::CircleAccess { circle_id, access } => {
344 validate_circle_access_ref(*circle_id, family, access)?;
345 Ok(())
346 }
347 CandidateExclusiveObjectRef::CircleEpochCloseIntent {
348 circle_id,
349 reference,
350 } => {
351 let expected = format!(
352 "{}.json",
353 crate::circle::circle_epoch_close_intent_semantic_prefix(
354 *circle_id,
355 reference.close_id,
356 reference.intent_hash,
357 )
358 );
359 if reference.object.slot().logical_key() != expected {
360 return Err(StoreProtocolError::RelocatedCandidateObject {
361 expected,
362 actual: reference.object.slot().logical_key().to_string(),
363 });
364 }
365 Ok(())
366 }
367 CandidateExclusiveObjectRef::CircleEpochCloseOutcome {
368 circle_id,
369 reference,
370 } => {
371 let expected = format!(
372 "{}.json",
373 crate::circle::circle_epoch_close_outcome_semantic_prefix(
374 *circle_id,
375 reference.close_id,
376 )
377 );
378 if reference.object.slot().logical_key() != expected {
379 return Err(StoreProtocolError::RelocatedCandidateObject {
380 expected,
381 actual: reference.object.slot().logical_key().to_string(),
382 });
383 }
384 Ok(())
385 }
386 CandidateExclusiveObjectRef::CircleEpochCloseCancellation {
387 circle_id,
388 reference,
389 } => {
390 let expected = format!(
391 "{}.json",
392 crate::circle::circle_epoch_close_outcome_semantic_prefix(
393 *circle_id,
394 reference.close_id,
395 )
396 );
397 if reference.object.slot().logical_key() != expected {
398 return Err(StoreProtocolError::RelocatedCandidateObject {
399 expected,
400 actual: reference.object.slot().logical_key().to_string(),
401 });
402 }
403 Ok(())
404 }
405 }
406}
407
408pub(super) fn validate_circle_access_ref(
409 circle_id: CircleId,
410 family: CandidateFamilyId,
411 access: &CircleAccessObjectRef,
412) -> Result<(), StoreProtocolError> {
413 if access.leaf.owner_pubkey != access.envelope.owner_pubkey
414 || access.leaf.recipient_slot != access.envelope.recipient_slot
415 || access.leaf.leaf_id != access.envelope.leaf_id
416 || access.leaf.leaf_hash != access.envelope.leaf_hash
417 || access.leaf.leaf_hash != access.leaf.object.stored_hash()
418 {
419 return Err(StoreProtocolError::Malformed(
420 "paired Circle access leaf and envelope references differ".to_string(),
421 ));
422 }
423 let leaf_expected = circle_access_leaf_semantic_prefix(
424 circle_id,
425 family,
426 &access.leaf.owner_pubkey,
427 access.leaf.epoch_id,
428 &access.leaf.recipient_slot,
429 access.leaf.leaf_id,
430 );
431 if access.leaf.object.slot().logical_key() != leaf_expected {
432 return Err(StoreProtocolError::RelocatedCandidateObject {
433 expected: leaf_expected,
434 actual: access.leaf.object.slot().logical_key().to_string(),
435 });
436 }
437 let envelope_expected = format!(
438 "{}.json",
439 circle_access_envelope_semantic_prefix(
440 circle_id,
441 family,
442 &access.envelope.owner_pubkey,
443 &access.envelope.recipient_slot,
444 access.envelope.control_hash,
445 )
446 );
447 if access.envelope.object.slot().logical_key() != envelope_expected {
448 return Err(StoreProtocolError::RelocatedCandidateObject {
449 expected: envelope_expected,
450 actual: access.envelope.object.slot().logical_key().to_string(),
451 });
452 }
453 Ok(())
454}
455
456pub(super) fn package_ref(
457 semantic_prefix: &str,
458 input: &StorePackageInput<'_>,
459) -> Result<StorePackageRef, StoreProtocolError> {
460 let package_bytes = input.bytes;
461 let changeset_size =
462 u64::try_from(package_bytes.len()).map_err(|_| StoreProtocolError::PackageTooLarge)?;
463 let content_hash = ObjectHash::digest(package_bytes);
464 let expected_key = format!("{semantic_prefix}.pkg");
465 if input.object.slot().logical_key() != expected_key {
466 return Err(StoreProtocolError::RelocatedPackage {
467 expected: expected_key,
468 actual: input.object.slot().logical_key().to_string(),
469 });
470 }
471 Ok(StorePackageRef {
472 candidate_family: input.candidate_family,
473 content_hash,
474 schema_version: input.schema_version,
475 changeset_size,
476 object: input.object.clone(),
477 })
478}
479
480pub(super) fn verify_package_ref(
481 package: &StorePackageRef,
482 package_bytes: &[u8],
483) -> Result<(), StoreProtocolError> {
484 let length =
485 u64::try_from(package_bytes.len()).map_err(|_| StoreProtocolError::PackageTooLarge)?;
486 if length != package.changeset_size {
487 return Err(StoreProtocolError::PackageLengthMismatch {
488 expected: package.changeset_size,
489 actual: length,
490 });
491 }
492 let actual = ObjectHash::digest(package_bytes);
493 if actual != package.content_hash {
494 return Err(StoreProtocolError::PackageHashMismatch {
495 expected: package.content_hash,
496 actual,
497 });
498 }
499 Ok(())
500}
501
502pub(super) fn validate_control(
503 author_registration: &StoreDeviceRegistrationRef,
504 author_pubkey: &str,
505 _membership_state: &StoreMembershipStateRef,
506 control: Option<&StoreControl>,
507) -> Result<(), StoreProtocolError> {
508 let Some(control) = control else {
509 return Ok(());
510 };
511 let transition = &control.transition;
512 if transition.body.author_registration != *author_registration
513 || transition.body.entry.coord.author_pubkey != author_pubkey
514 || transition.body.entry.coord.seq == 0
515 {
516 return Err(StoreProtocolError::InvalidMergeMembershipControl);
517 }
518 Ok(())
519}
520
521pub(super) fn validate_parsed_control(
522 commit: &StoreBatchCommit,
523 author: &StoreDeviceRegistration,
524) -> Result<(), StoreProtocolError> {
525 validate_control(
526 &commit.author_registration,
527 &author.author_pubkey,
528 &commit.membership_state,
529 commit.control(),
530 )
531}
532
533pub(super) fn validate_circle_control_coord(
534 coord: &CircleControlCoord,
535) -> Result<(), StoreProtocolError> {
536 coord
537 .validate()
538 .map_err(|_| StoreProtocolError::InvalidCircleControlCoord)?;
539 Ok(())
540}
541
542pub(super) fn validate_circle_control_refs(
543 controls: &[CircleControlRef],
544) -> Result<(), StoreProtocolError> {
545 let mut seen = BTreeSet::new();
546 for control_ref in controls {
547 if !seen.insert(control_ref.circle_id()) {
548 return Err(StoreProtocolError::DuplicateCircleControl(
549 control_ref.circle_id(),
550 ));
551 }
552 validate_circle_control_coord(control_ref.control())?;
553 }
554 Ok(())
555}
556
557impl StoreBatchCommit {
558 pub fn commit_hash(&self) -> ObjectHash {
559 self.hash()
560 }
561
562 pub fn verify_at(
563 &self,
564 expected_store_root_hash: ObjectHash,
565 expected_coord: &StoreCommitCoord,
566 author: &StoreDeviceRegistration,
567 ) -> Result<(), StoreProtocolError> {
568 self.require_version()?;
569 crate::objects::verify_store_root(expected_store_root_hash, self.store_root_hash)?;
570 self.publication_base
571 .validate_for_store(expected_store_root_hash)?;
572 let stream_id = commit_stream_id(expected_coord);
573 if self.order.seq() != expected_coord.sequence() {
574 return Err(StoreProtocolError::RelocatedSlot {
575 expected: commit_slot_prefix(&stream_id, expected_coord.sequence()),
576 actual: commit_slot_prefix(&stream_id, self.order.seq()),
577 });
578 }
579 self.author_registration.verify_registration(author)?;
580 let family = self.candidate_family();
581 if let Some(package) = self.store_package() {
582 if package.candidate_family != self.candidate_family() {
583 return Err(StoreProtocolError::Malformed(
584 "Store package candidate family differs from its commit".to_string(),
585 ));
586 }
587 let expected =
588 package_semantic_prefix(family, &stream_id, self.order.seq(), package.content_hash);
589 if package.object.slot().logical_key() != format!("{expected}.pkg") {
590 return Err(StoreProtocolError::RelocatedPackage {
591 expected,
592 actual: package.object.slot().logical_key().to_string(),
593 });
594 }
595 }
596 let mut seen_circles = BTreeSet::new();
597 for circle_package in self.circle_packages() {
598 if circle_package.package.candidate_family != self.candidate_family() {
599 return Err(StoreProtocolError::Malformed(
600 "Circle package candidate family differs from its commit".to_string(),
601 ));
602 }
603 if !seen_circles.insert(circle_package.circle_id) {
604 return Err(StoreProtocolError::DuplicateCirclePackage(
605 circle_package.circle_id,
606 ));
607 }
608 validate_circle_control_coord(&circle_package.control)?;
609 let expected = circle_package_semantic_prefix(
610 circle_package.circle_id,
611 family,
612 &stream_id,
613 self.seq(),
614 circle_package.package.content_hash,
615 );
616 if circle_package.package.object.slot().logical_key() != format!("{expected}.pkg") {
617 return Err(StoreProtocolError::RelocatedCirclePackage {
618 circle_id: circle_package.circle_id,
619 expected,
620 actual: circle_package
621 .package
622 .object
623 .slot()
624 .logical_key()
625 .to_string(),
626 });
627 }
628 }
629 validate_commit_body(self.store_root_hash, &self.body, &self.author_registration)?;
630 if matches!(self.body, StoreCommitBody::Operations(_)) {
631 validate_operation_membership_authority(
632 self.membership_authority.as_ref().ok_or_else(|| {
633 StoreProtocolError::Malformed(
634 "operations commit omits membership authority".to_string(),
635 )
636 })?,
637 )?;
638 }
639 if let StoreCommitBody::AbandonCandidates { manifests } = &self.body {
640 validate_candidate_abandonment(
641 manifests,
642 self.store_root_hash,
643 &self.author_registration,
644 expected_coord,
645 &self.order,
646 author,
647 )?;
648 }
649 if let StoreCommitBody::OwnerPromotionRequest { request } = &self.body {
650 validate_owner_promotion_request_for_commit(
651 request,
652 self.store_root_hash,
653 &self.author_registration,
654 author,
655 &self.membership_state,
656 &self.device_state,
657 )?;
658 }
659 self.verified_candidate_objects()?;
660 validate_commit_order(&self.order)?;
661 validate_commit_predecessor_states(
662 &self.order,
663 &self.membership_state,
664 &self.device_state,
665 )?;
666 if let Some(authority) = self.membership_authority.as_ref() {
667 validate_membership_authority(authority)?;
668 }
669 validate_parsed_control(self, author)?;
670 self.verify_by(&author.device_signing_pubkey)?;
671 Ok(())
672 }
673
674 pub fn verify_store_package(&self, package_bytes: &[u8]) -> Result<(), StoreProtocolError> {
675 let package = self
676 .store_package()
677 .ok_or(StoreProtocolError::MissingStorePackage)?;
678 verify_package_ref(package, package_bytes)
679 }
680
681 pub fn verify_circle_package(
682 &self,
683 circle_id: CircleId,
684 package_bytes: &[u8],
685 ) -> Result<(), StoreProtocolError> {
686 let package = self
687 .circle_packages()
688 .iter()
689 .find(|package| package.circle_id == circle_id)
690 .ok_or(StoreProtocolError::MissingCirclePackage(circle_id))?;
691 verify_package_ref(&package.package, package_bytes)
692 }
693
694 #[cfg(any(test, feature = "test-utils"))]
695 pub fn operations_membership_authority(
696 &self,
697 ) -> Result<StoreOperationMembershipAuthority, StoreProtocolError> {
698 if self.operations().is_none() {
699 return Err(StoreProtocolError::Malformed(
700 "Store commit does not carry operations".to_string(),
701 ));
702 }
703 let predecessor = self.membership_authority.clone().ok_or_else(|| {
704 StoreProtocolError::Malformed(
705 "operations commit omits its predecessor membership grant authority".to_string(),
706 )
707 })?;
708 validate_operation_membership_authority(&predecessor)?;
709 Ok(StoreOperationMembershipAuthority { predecessor })
710 }
711}