1use super::drafts::*;
2use super::*;
3
4impl CircleTransitionDraft {
5 #[allow(clippy::too_many_arguments)]
6 pub fn founder(
7 store_root_hash: ObjectHash,
8 candidate_family: crate::store_commit::CandidateFamilyId,
9 device_id: &str,
10 name: &str,
11 metadata_stamp: &str,
12 store_membership: StoreMembershipStateRef,
13 membership_authority: MembershipGrantCreationAuthority,
14 mut store_members: Vec<(String, MemberRole)>,
15 ids: &dyn coven_foundation::id_provider::IdProvider,
16 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
17 ) -> Result<Self, CircleTransitionError> {
18 let author_pubkey = keys::public_key_hex(signer);
19 store_members.sort_by(|left, right| left.0.cmp(&right.0));
20 store_members.dedup_by(|left, right| left.0 == right.0);
21 if !store_members
22 .iter()
23 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
24 {
25 return Err(CircleTransitionError::AuthorNotStoreWriter);
26 }
27 let owner_grant =
28 MembershipGrantId(generated_id_digest(ids, OWNER_GRANT_ID_GENERATION_DOMAIN));
29 let author_stream_id = AuthorStreamId::from_digest(generated_id_digest(
30 ids,
31 b"coven.circle-transition-draft-stream.v1\0",
32 ));
33 let circle_id = CircleId::founder(store_root_hash, &author_pubkey, &owner_grant);
34 let epoch_id = CircleEpochId::generate(ids);
35 let keyring = MasterKeyring::generate();
36 let encryption = EncryptionService::from(keyring.clone());
37 let key_fingerprint = encryption.seal_key_fingerprint();
38 let entry = CircleRosterEntry::founder(
39 store_root_hash,
40 circle_id,
41 device_id,
42 author_stream_id,
43 owner_grant.clone(),
44 signer,
45 );
46 let roster_objects = FounderRosterObjects {
47 resolved: CircleRosterChain::from_entries(vec![entry.clone()])?.resolved(),
48 entry,
49 };
50 let roster_state = MergeCircleRosterStateRef {
51 heads: Vec::new(),
52 resolutions: Vec::new(),
53 state_hash: roster_objects.resolved.state_hash,
54 };
55 let metadata = CircleMetadata::founder(
56 store_root_hash,
57 circle_id,
58 epoch_id,
59 name,
60 metadata_stamp,
61 device_id,
62 author_stream_id,
63 owner_grant.clone(),
64 roster_state.clone(),
65 key_fingerprint,
66 signer,
67 )?;
68 let metadata_state = MergeCircleMetadataStateRef {
69 heads: Vec::new(),
70 selected: metadata.coord(),
71 state_hash: metadata.metadata_hash(),
72 };
73 let roster = roster_objects.resolved.clone();
74 let access = CircleAccessDraft::prepare(
75 store_root_hash,
76 candidate_family,
77 circle_id,
78 epoch_id,
79 &keyring.to_serialized(),
80 key_fingerprint,
81 &roster_state,
82 &roster.members(),
83 &store_membership,
84 &store_members,
85 &std::collections::BTreeMap::new(),
86 ids,
87 signer,
88 )?;
89 let common = ActiveCircleEpochCore {
90 epoch_id,
91 key_fingerprint,
92 owners: vec![author_pubkey.clone()],
93 access_root: access.access_root(),
94 origin: CircleEpochOrigin::Founder,
95 };
96 let value = CircleControlValue {
97 order: MergeCircleControlOrder {
98 device_id: device_id.to_string(),
99 stream_id: author_stream_id,
100 author_owner_grant: owner_grant.clone(),
101 seq: 1,
102 previous_control_hash: None,
103 dependencies: Vec::new(),
104 },
105 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
106 common,
107 metadata: metadata_state,
108 roster: roster_state.clone(),
109 store_membership,
110 covered_control_heads: Vec::new(),
111 }),
112 author_authority: MergeCircleOwnerAuthorityRef::Roster {
113 roster: roster_state,
114 grant_id: owner_grant.clone(),
115 created_at: roster_objects.entry.coord(),
116 },
117 membership_authority,
118 };
119 let control_value = CircleControlBody {
120 store_root_hash,
121 circle_id,
122 value,
123 author_pubkey: author_pubkey.clone(),
124 };
125 let control_value = Signed::sign(control_value, signer);
126 let control = PreparedCircleControl {
127 coord: control_value.coord(),
128 bytes: serde_json::to_vec(&control_value)
129 .expect("circle control serialization cannot fail"),
130 value: control_value,
131 };
132 let policy_objects = CircleTransitionDraftPolicy {
133 roster: CircleRosterDraftPolicy::Founder {
134 entry: roster_objects.entry,
135 },
136 metadata_successor: true,
137 };
138 let access = access.finish(&control)?;
139 Ok(Self {
140 circle_id,
141 epoch_id,
142 keyring: keyring.to_serialized(),
143 roster,
144 policy: policy_objects,
145 metadata,
146 close_intent: None,
147 close_finalization: None,
148 close_cancellation: None,
149 access,
150 control,
151 })
152 }
153
154 #[allow(clippy::too_many_arguments)]
155 pub fn add_member(
156 candidate_family: crate::store_commit::CandidateFamilyId,
157 device_id: &str,
158 store_membership: StoreMembershipStateRef,
159 membership_authority: MembershipGrantCreationAuthority,
160 store_members: Vec<(String, MemberRole)>,
161 current_control: &PreparedCircleControl,
162 current_roster: &CircleMaterializedRoster,
163 current_roster_chain: CircleRosterChain,
164 current_metadata: &CircleMetadata,
165 keyring: &str,
166 roster_stream: AuthorStreamId,
167 member_pubkey: String,
168 role: crate::circle::CircleRole,
169 bootstrap: CircleBootstrapRef,
170 ids: &dyn coven_foundation::id_provider::IdProvider,
171 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
172 ) -> Result<Self, CircleTransitionError> {
173 if current_roster_chain.try_resolved()? != *current_roster {
174 return Err(CircleTransitionError::InvalidCurrentState);
175 }
176 let context = circle_successor_context(
177 store_members,
178 current_control,
179 current_roster,
180 current_metadata,
181 keyring,
182 signer,
183 )?;
184 let CircleSuccessorContext {
185 store_members,
186 author_pubkey,
187 epoch: active_epoch,
188 grant_id,
189 author_authority,
190 key_fingerprint,
191 } = context;
192 if !store_members
193 .iter()
194 .any(|(pubkey, _)| pubkey == &member_pubkey)
195 {
196 return Err(CircleTransitionError::MemberNotInStore(member_pubkey));
197 }
198 let entry = current_roster_chain.signed_set_member(
199 device_id,
200 roster_stream,
201 member_pubkey.clone(),
202 role,
203 signer,
204 )?;
205 let roster = current_roster_chain.resolved_with_successor(entry.clone())?;
206 let roster_state = MergeCircleRosterStateRef {
207 heads: active_epoch.roster.heads.clone(),
208 resolutions: active_epoch.roster.resolutions.clone(),
209 state_hash: roster.state_hash,
210 };
211 let store_root_hash = current_control.value.store_root_hash;
212 let circle_id = current_control.value.circle_id;
213 let epoch_id = current_control.value.epoch_id();
214 let mut control_value = CircleControlBody {
215 store_root_hash,
216 circle_id,
217 value: CircleControlValue {
218 order: MergeCircleControlOrder {
219 device_id: device_id.to_string(),
220 stream_id: roster_stream,
221 author_owner_grant: grant_id.clone(),
222 seq: current_control
223 .value
224 .ordinal()
225 .checked_add(1)
226 .ok_or(CircleTransitionError::SequenceOverflow)?,
227 previous_control_hash: Some(current_control.coord.control_hash()),
228 dependencies: vec![current_control.coord.clone()],
229 },
230 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
231 common: active_epoch.common.clone(),
232 metadata: active_epoch.metadata.clone(),
233 roster: roster_state.clone(),
234 store_membership,
235 covered_control_heads: active_epoch.covered_control_heads.clone(),
236 }),
237 author_authority,
238 membership_authority,
239 },
240 author_pubkey,
241 };
242 let bootstraps =
243 std::collections::BTreeMap::from([(member_pubkey.clone(), bootstrap.clone())]);
244 let access = CircleAccessDraft::prepare(
245 store_root_hash,
246 candidate_family,
247 circle_id,
248 epoch_id,
249 keyring,
250 key_fingerprint,
251 &roster_state,
252 &roster.members(),
253 &control_value
254 .value
255 .state
256 .active_epoch()
257 .expect("member addition constructs an active epoch")
258 .store_membership,
259 &store_members,
260 &bootstraps,
261 ids,
262 signer,
263 )?;
264 control_value
265 .value
266 .state
267 .active_epoch_mut()
268 .expect("member addition constructs an active epoch")
269 .common
270 .access_root = access.access_root();
271 let control_value = Signed::sign(control_value, signer);
272 let control = PreparedCircleControl {
273 coord: control_value.coord(),
274 bytes: serde_json::to_vec(&control_value)
275 .expect("circle control serialization cannot fail"),
276 value: control_value,
277 };
278 let access = access.finish(&control)?;
279 Ok(Self {
280 circle_id,
281 epoch_id,
282 keyring: keyring.to_string(),
283 roster,
284 policy: CircleTransitionDraftPolicy {
285 roster: CircleRosterDraftPolicy::Successor {
286 predecessor: current_roster_chain,
287 entry,
288 },
289 metadata_successor: false,
290 },
291 metadata: current_metadata.clone(),
292 close_intent: None,
293 close_finalization: None,
294 close_cancellation: None,
295 access,
296 control,
297 })
298 }
299
300 #[allow(clippy::too_many_arguments)]
301 pub fn rename(
302 candidate_family: crate::store_commit::CandidateFamilyId,
303 device_id: &str,
304 name: &str,
305 metadata_stamp: &str,
306 store_membership: StoreMembershipStateRef,
307 membership_authority: MembershipGrantCreationAuthority,
308 store_members: Vec<(String, MemberRole)>,
309 current_control: &PreparedCircleControl,
310 current_roster: &CircleMaterializedRoster,
311 current_metadata: &CircleMetadata,
312 keyring: &str,
313 ids: &dyn coven_foundation::id_provider::IdProvider,
314 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
315 ) -> Result<Self, CircleTransitionError> {
316 if name.trim().is_empty() {
317 return Err(CircleTransitionError::EmptyName);
318 }
319 let context = circle_successor_context(
320 store_members,
321 current_control,
322 current_roster,
323 current_metadata,
324 keyring,
325 signer,
326 )?;
327 let CircleSuccessorContext {
328 store_members,
329 author_pubkey,
330 epoch: active_epoch,
331 grant_id,
332 author_authority,
333 key_fingerprint,
334 } = context;
335 let store_root_hash = current_control.value.store_root_hash;
336 let circle_id = current_control.value.circle_id;
337 let epoch_id = current_control.value.epoch_id();
338 let roster_state = current_control.value.roster_state_ref();
339
340 let own_head = active_epoch.metadata.heads.iter().find(|head| {
341 head.coord.author_pubkey == author_pubkey
342 && head.coord.device_id == device_id
343 && head.coord.author_owner_grant == grant_id
344 });
345 let author_stream_id = own_head.map_or_else(
346 || {
347 AuthorStreamId::from_digest(generated_id_digest(
348 ids,
349 b"coven.circle-transition-draft-stream.v1\0",
350 ))
351 },
352 |head| head.coord.stream_id,
353 );
354 let metadata_seq = match own_head {
355 Some(head) => head
356 .coord
357 .seq
358 .checked_add(1)
359 .ok_or(CircleTransitionError::SequenceOverflow)?,
360 None => 1,
361 };
362 let metadata_previous = own_head.map(|head| head.coord.metadata_hash);
363 let metadata_dependencies = active_epoch
364 .metadata
365 .heads
366 .iter()
367 .map(|head| head.coord.clone())
368 .collect::<Vec<_>>();
369 let metadata_state = active_epoch.metadata.clone();
370 let mut control_value = CircleControlValue {
371 order: MergeCircleControlOrder {
372 device_id: device_id.to_string(),
373 stream_id: author_stream_id,
374 author_owner_grant: grant_id.clone(),
375 seq: current_control
376 .value
377 .ordinal()
378 .checked_add(1)
379 .ok_or(CircleTransitionError::SequenceOverflow)?,
380 previous_control_hash: Some(current_control.coord.control_hash()),
381 dependencies: vec![current_control.coord.clone()],
382 },
383 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
384 common: active_epoch.common.clone(),
385 metadata: active_epoch.metadata.clone(),
386 roster: active_epoch.roster.clone(),
387 store_membership,
388 covered_control_heads: active_epoch.covered_control_heads.clone(),
389 }),
390 author_authority,
391 membership_authority,
392 };
393 let author_owner_grant = grant_id.clone();
394
395 let metadata = Signed::sign(
396 CircleMetadataBody {
397 store_root_hash,
398 circle_id,
399 epoch_id,
400 name: name.to_string(),
401 seq: metadata_seq,
402 previous_hash: metadata_previous,
403 dependencies: metadata_dependencies,
404 metadata_stamp: metadata_stamp.to_string(),
405 author_pubkey: author_pubkey.clone(),
406 device_id: device_id.to_string(),
407 stream_id: author_stream_id,
408 author_owner_grant,
409 author_roster: roster_state.clone(),
410 key_fingerprint,
411 },
412 signer,
413 );
414
415 let mut metadata_state = metadata_state;
416 let selected = [current_metadata, &metadata]
417 .into_iter()
418 .max_by_key(|entry| {
419 (
420 entry.metadata_stamp.as_str(),
421 entry.author_pubkey.as_str(),
422 entry.device_id.as_str(),
423 entry.metadata_hash(),
424 )
425 })
426 .expect("current and successor metadata are non-empty");
427 metadata_state.selected = selected.coord();
428 metadata_state.state_hash = selected.metadata_hash();
429
430 let access = CircleAccessDraft::prepare(
431 store_root_hash,
432 candidate_family,
433 circle_id,
434 epoch_id,
435 keyring,
436 key_fingerprint,
437 &roster_state,
438 ¤t_roster.members(),
439 &control_value
440 .state
441 .active_epoch()
442 .expect("rename constructs an active epoch")
443 .store_membership,
444 &store_members,
445 &std::collections::BTreeMap::new(),
446 ids,
447 signer,
448 )?;
449 let active_epoch = control_value
450 .state
451 .active_epoch_mut()
452 .expect("rename constructs an active epoch");
453 active_epoch.common.access_root = access.access_root();
454 active_epoch.metadata = metadata_state;
455 let control_value = CircleControlBody {
456 store_root_hash,
457 circle_id,
458 value: control_value,
459 author_pubkey,
460 };
461 let control_value = Signed::sign(control_value, signer);
462 let control = PreparedCircleControl {
463 coord: control_value.coord(),
464 bytes: serde_json::to_vec(&control_value)
465 .expect("circle control serialization cannot fail"),
466 value: control_value,
467 };
468 let policy_objects = CircleTransitionDraftPolicy {
469 roster: CircleRosterDraftPolicy::Inherited,
470 metadata_successor: true,
471 };
472 let access = access.finish(&control)?;
473 Ok(Self {
474 circle_id,
475 epoch_id,
476 keyring: keyring.to_string(),
477 roster: current_roster.clone(),
478 policy: policy_objects,
479 metadata,
480 close_intent: None,
481 close_finalization: None,
482 close_cancellation: None,
483 access,
484 control,
485 })
486 }
487
488 #[allow(clippy::too_many_arguments)]
503 pub fn resolve(
504 candidate_family: crate::store_commit::CandidateFamilyId,
505 device_id: &str,
506 store_membership: StoreMembershipStateRef,
507 membership_authority: MembershipGrantCreationAuthority,
508 store_members: Vec<(String, MemberRole)>,
509 chosen_control: &PreparedCircleControl,
510 chosen_roster: &CircleMaterializedRoster,
511 chosen_metadata: &CircleMetadata,
512 keyring: &str,
513 losing_branches: Vec<ResolvedConflictBranch>,
514 ids: &dyn coven_foundation::id_provider::IdProvider,
515 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
516 ) -> Result<Self, CircleTransitionError> {
517 let context = circle_successor_context(
518 store_members,
519 chosen_control,
520 chosen_roster,
521 chosen_metadata,
522 keyring,
523 signer,
524 )?;
525 let CircleSuccessorContext {
526 store_members,
527 author_pubkey,
528 epoch: active_epoch,
529 grant_id: _,
530 author_authority,
531 key_fingerprint,
532 } = context;
533 let store_root_hash = chosen_control.value.store_root_hash;
534 let circle_id = chosen_control.value.circle_id;
535 let epoch_id = chosen_control.value.epoch_id();
536 let roster_state = chosen_control.value.roster_state_ref();
537
538 let mut covered_control_heads = active_epoch.covered_control_heads.clone();
546 let mut metadata = active_epoch.metadata.clone();
547 let mut roster = active_epoch.roster.clone();
548 for branch in &losing_branches {
549 merge_frontier_head(
550 &mut covered_control_heads,
551 branch.control_head.clone(),
552 |head| head.coord.stream_key(),
553 |head| head.coord.seq,
554 );
555 for head in &branch.metadata_heads {
556 merge_frontier_head(
557 &mut metadata.heads,
558 head.clone(),
559 |head| head.coord.stream_key(),
560 |head| head.coord.seq,
561 );
562 }
563 for head in &branch.roster_heads {
564 merge_frontier_head(
565 &mut roster.heads,
566 head.clone(),
567 |head| head.coord.stream_key(),
568 |head| head.coord.seq,
569 );
570 }
571 }
572 covered_control_heads.sort_by_key(|head| head.coord.stream_key());
573 metadata.heads.sort_by_key(|head| head.coord.stream_key());
574 roster.heads.sort_by_key(|head| head.coord.stream_key());
575
576 let selected_metadata = std::iter::once(chosen_metadata)
581 .chain(
582 losing_branches
583 .iter()
584 .map(|branch| &branch.selected_metadata),
585 )
586 .max_by_key(|entry| {
587 (
588 entry.metadata_stamp.clone(),
589 entry.author_pubkey.clone(),
590 entry.device_id.clone(),
591 entry.metadata_hash(),
592 )
593 })
594 .expect("a resolution names at least the chosen branch's metadata")
595 .clone();
596 metadata.selected = selected_metadata.coord();
597 metadata.state_hash = selected_metadata.metadata_hash();
598
599 let mut control_value = CircleControlBody {
600 store_root_hash,
601 circle_id,
602 value: CircleControlValue {
603 order: MergeCircleControlOrder {
604 device_id: device_id.to_string(),
605 stream_id: active_epoch
606 .covered_control_heads
607 .iter()
608 .find(|head| head.coord.stream_key().author_pubkey == author_pubkey)
609 .map_or_else(
610 || {
611 AuthorStreamId::from_digest(generated_id_digest(
612 ids,
613 b"coven.circle-transition-draft-stream.v1\0",
614 ))
615 },
616 |head| head.coord.stream_id,
617 ),
618 author_owner_grant: chosen_metadata.author_owner_grant.clone(),
619 seq: chosen_control
620 .value
621 .ordinal()
622 .checked_add(1)
623 .ok_or(CircleTransitionError::SequenceOverflow)?,
624 previous_control_hash: Some(chosen_control.coord.control_hash()),
625 dependencies: vec![chosen_control.coord.clone()],
626 },
627 state: CircleControlState::ActiveEpoch(MergeActiveCircleEpoch {
628 common: active_epoch.common.clone(),
629 metadata,
630 roster,
631 store_membership,
632 covered_control_heads,
633 }),
634 author_authority,
635 membership_authority,
636 },
637 author_pubkey,
638 };
639 let access = CircleAccessDraft::prepare(
640 store_root_hash,
641 candidate_family,
642 circle_id,
643 epoch_id,
644 keyring,
645 key_fingerprint,
646 &roster_state,
647 &chosen_roster.members(),
648 &control_value
649 .value
650 .state
651 .active_epoch()
652 .expect("control resolution constructs an active epoch")
653 .store_membership,
654 &store_members,
655 &std::collections::BTreeMap::new(),
656 ids,
657 signer,
658 )?;
659 control_value
660 .value
661 .state
662 .active_epoch_mut()
663 .expect("control resolution constructs an active epoch")
664 .common
665 .access_root = access.access_root();
666 let control_value = Signed::sign(control_value, signer);
667 let control = PreparedCircleControl {
668 coord: control_value.coord(),
669 bytes: serde_json::to_vec(&control_value)
670 .expect("circle control serialization cannot fail"),
671 value: control_value,
672 };
673 let access = access.finish(&control)?;
674 Ok(Self {
675 circle_id,
676 epoch_id,
677 keyring: keyring.to_string(),
678 roster: chosen_roster.clone(),
679 policy: CircleTransitionDraftPolicy {
680 roster: CircleRosterDraftPolicy::Inherited,
681 metadata_successor: false,
682 },
683 metadata: selected_metadata,
684 close_intent: None,
685 close_finalization: None,
686 close_cancellation: None,
687 access,
688 control,
689 })
690 }
691
692 #[allow(clippy::too_many_arguments)]
698 pub fn delete(
699 device_id: &str,
700 store_membership: StoreMembershipStateRef,
701 membership_authority: MembershipGrantCreationAuthority,
702 store_members: Vec<(String, MemberRole)>,
703 current_control: &PreparedCircleControl,
704 current_roster: &CircleMaterializedRoster,
705 current_metadata: &CircleMetadata,
706 keyring: &str,
707 ids: &dyn coven_foundation::id_provider::IdProvider,
708 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
709 ) -> Result<Self, CircleTransitionError> {
710 let context = circle_delete_successor_context(
711 store_members,
712 current_control,
713 current_roster,
714 current_metadata,
715 keyring,
716 signer,
717 )?;
718 let CircleSuccessorContext {
719 store_members: _,
720 author_pubkey,
721 epoch,
722 grant_id,
723 author_authority,
724 key_fingerprint: _,
725 } = context;
726 let store_root_hash = current_control.value.store_root_hash;
727 let circle_id = current_control.value.circle_id;
728 let epoch_id = current_control.value.epoch_id();
729 let frozen_epoch = MergeActiveCircleEpoch {
730 common: epoch.common.clone(),
731 metadata: epoch.metadata.clone(),
732 roster: epoch.roster.clone(),
733 store_membership,
734 covered_control_heads: epoch.covered_control_heads.clone(),
735 };
736 let stream_id = epoch
737 .covered_control_heads
738 .iter()
739 .find(|head| head.coord.stream_key().author_pubkey == author_pubkey)
740 .map_or_else(
741 || {
742 AuthorStreamId::from_digest(generated_id_digest(
743 ids,
744 b"coven.circle-transition-draft-stream.v1\0",
745 ))
746 },
747 |head| head.coord.stream_id,
748 );
749 let control_value = CircleControlBody {
750 store_root_hash,
751 circle_id,
752 value: CircleControlValue {
753 order: MergeCircleControlOrder {
754 device_id: device_id.to_string(),
755 stream_id,
756 author_owner_grant: grant_id,
757 seq: current_control
758 .value
759 .ordinal()
760 .checked_add(1)
761 .ok_or(CircleTransitionError::SequenceOverflow)?,
762 previous_control_hash: Some(current_control.coord.control_hash()),
763 dependencies: vec![current_control.coord.clone()],
764 },
765 state: CircleControlState::Deleted(DeletedCircle { frozen_epoch }),
766 author_authority,
767 membership_authority,
768 },
769 author_pubkey,
770 };
771 let control_value = Signed::sign(control_value, signer);
772 let control = PreparedCircleControl {
773 coord: control_value.coord(),
774 bytes: serde_json::to_vec(&control_value)
775 .expect("circle control serialization cannot fail"),
776 value: control_value,
777 };
778 Ok(Self {
779 circle_id,
780 epoch_id,
781 keyring: keyring.to_string(),
782 roster: current_roster.clone(),
783 policy: CircleTransitionDraftPolicy {
784 roster: CircleRosterDraftPolicy::Inherited,
785 metadata_successor: false,
786 },
787 metadata: current_metadata.clone(),
788 close_intent: None,
789 close_finalization: None,
790 close_cancellation: None,
791 access: Vec::new(),
792 control,
793 })
794 }
795}