1use std::collections::BTreeMap;
6
7use crate::circle::{CircleControlHead, PreparedCircleControl};
8use crate::circle_control::StoreMembershipStateRef;
9use crate::store_commit::ObjectHash;
10use crate::{membership, store_commit};
11use coven_keys::keys::{self, UserKeypair};
12
13pub(crate) fn exact_object(label: &str, bytes: &[u8]) -> crate::objects::ExactObjectRef {
14 crate::objects::ExactObjectRef::new(
15 crate::objects::ObjectSlot::logical(format!("store-v1/test/{label}.json")).unwrap(),
16 bytes.len() as u64,
17 ObjectHash::digest(bytes),
18 )
19}
20
21pub fn exact_logical_object(logical_key: String, bytes: &[u8]) -> crate::objects::ExactObjectRef {
22 crate::objects::ExactObjectRef::new(
23 crate::objects::ObjectSlot::logical(logical_key).unwrap(),
24 bytes.len() as u64,
25 ObjectHash::digest(bytes),
26 )
27}
28
29pub(crate) fn test_founder_entry(
30 label: &str,
31 owner: &UserKeypair,
32 membership: store_commit::GrantStreamAnchor,
33) -> membership::MembershipEntry {
34 membership::founder_entry(
35 label,
36 owner,
37 crate::causal_grants::MembershipGrantId::from_test_label(label),
38 "founder",
39 membership,
40 crate::provider::FounderProviderAdminGrant::from_test_label(label),
41 )
42}
43
44pub fn merge_membership_ref(
45 owner: &UserKeypair,
46 members: &[(String, membership::MemberRole)],
47 label: &str,
48) -> (StoreMembershipStateRef, membership::MembershipCoord) {
49 let founder = test_founder_entry(
50 label,
51 owner,
52 store_commit::GrantStreamAnchor::StoreMembership {
53 first_slot: crate::objects::ObjectSlot::logical(format!(
54 "store-v1/test/{label}/membership/1.json"
55 ))
56 .unwrap(),
57 },
58 );
59 let founder_coord = founder.coord();
60 let mut chain = membership::MembershipChain::from_entries(vec![founder])
61 .expect("found merge-concurrent membership");
62 for (index, (pubkey, role)) in members.iter().enumerate() {
63 if pubkey == &keys::public_key_hex(owner) {
64 continue;
65 }
66 if role == &membership::MemberRole::Owner {
67 chain
68 .add_owner_for_test(
69 owner,
70 founder_coord.stream_id,
71 pubkey.clone(),
72 format!("member-{index}"),
73 )
74 .expect("promote merge-concurrent Owner");
75 continue;
76 }
77 let entry = chain
78 .signed_set_member_in_stream(
79 owner,
80 founder_coord.stream_id,
81 pubkey.clone(),
82 None,
83 role.clone(),
84 format!("member-{index}"),
85 )
86 .expect("sign merge-concurrent member");
87 chain
88 .add_entry(entry)
89 .expect("apply merge-concurrent member");
90 }
91 let resolved = chain.resolved();
92 let tip = chain.entries().last().expect("membership tip").coord();
93 let head = membership::MembershipHeadRef {
94 coord: tip,
95 head_hash: ObjectHash::digest(format!("{label} head").as_bytes()),
96 object: exact_object(&format!("{label}/membership-head"), b"membership head"),
97 };
98 (
99 StoreMembershipStateRef::from_parts(vec![head], Vec::new(), resolved.state_hash)
100 .expect("valid merge-concurrent membership reference"),
101 founder_coord,
102 )
103}
104
105pub struct MergeDeviceAuthority {
106 registration: store_commit::StoreDeviceRegistration,
107 reference: store_commit::StoreDeviceRegistrationRef,
108 device_signer: UserKeypair,
109 stream_id: membership::AuthorStreamId,
110}
111
112impl MergeDeviceAuthority {
113 pub fn registration(&self) -> &store_commit::StoreDeviceRegistration {
114 &self.registration
115 }
116
117 pub fn reference(&self) -> &store_commit::StoreDeviceRegistrationRef {
118 &self.reference
119 }
120
121 pub fn stream_id(&self) -> membership::AuthorStreamId {
122 self.stream_id
123 }
124
125 #[allow(clippy::too_many_arguments)]
126 pub fn sign_operations(
127 &self,
128 store_root_hash: ObjectHash,
129 write_id: crate::write::WriteId,
130 coord: store_commit::StoreCommitCoord,
131 order: store_commit::StoreCommitOrder,
132 membership_state: StoreMembershipStateRef,
133 device_state: store_commit::StoreDeviceStateRef,
134 membership_authority: membership::MembershipCoord,
135 input: store_commit::StoreCommitOperationsInput<'_>,
136 ) -> Result<store_commit::StoreBatchCommit, store_commit::StoreProtocolError> {
137 store_commit::StoreBatchCommit::signed_operations(
138 store_root_hash,
139 write_id,
140 coord,
141 self.reference.clone(),
142 &self.registration,
143 order,
144 crate::store_commit::StorePublicationBase::Genesis,
145 membership_state,
146 device_state,
147 membership_authority,
148 input,
149 &self.device_signer,
150 )
151 }
152
153 pub fn circle_control_reference(
154 &self,
155 control: &PreparedCircleControl,
156 label: &str,
157 ) -> store_commit::CircleControlRef {
158 let control_object = exact_object(&format!("{label}/control"), &control.bytes);
159 let head_slot = crate::objects::ObjectSlot::logical(format!(
160 "store-v1/test/{label}/control-head/1.json"
161 ))
162 .expect("valid test Circle control-head slot");
163 let activation = store_commit::StreamActivation::grant_authorized(
164 control.value.store_root_hash,
165 self.reference.clone(),
166 control.value.author_grant_id(),
167 store_commit::GrantStreamAnchor::CircleControl {
168 circle_id: control.value.circle_id,
169 first_slot: head_slot.clone(),
170 },
171 );
172 let head = CircleControlHead::signed(
173 &control.value,
174 control_object.clone(),
175 store_commit::SuccessorLink {
176 activation: activation.activation_id(),
177 predecessor: None,
178 next_slot: crate::objects::ObjectSlot::logical(format!(
179 "store-v1/test/{label}/control-head/2.json"
180 ))
181 .expect("valid next test Circle control-head slot"),
182 },
183 &self.device_signer,
184 );
185 let head_bytes = serde_json::to_vec(&head).expect("serialize test Circle control head");
186 let head_object = crate::objects::ExactObjectRef::new(
187 head_slot,
188 head_bytes.len() as u64,
189 ObjectHash::digest(&head_bytes),
190 );
191 let objects = store_commit::CircleActivationObjects {
192 control: control_object,
193 close_intent: None,
194 close_outcome: None,
195 close_cancellation: None,
196 roster_entries: BTreeMap::new(),
197 roster_heads: Vec::new(),
198 roster_resolutions: BTreeMap::new(),
199 metadata_entries: BTreeMap::new(),
200 metadata_heads: Vec::new(),
201 access: Vec::new(),
202 };
203 store_commit::CircleControlRef {
204 circle_id: control.value.circle_id,
205 control: control.coord.clone(),
206 head_hash: head.head_hash(),
207 head_object,
208 objects,
209 }
210 }
211}
212
213pub fn merge_device_authority(
214 identity: &UserKeypair,
215 store_root_hash: ObjectHash,
216 label: &str,
217) -> MergeDeviceAuthority {
218 let root = store_commit::StoreRootRef {
219 store_root_id: ObjectHash::digest(format!("{label} identity").as_bytes()),
220 store_root_hash,
221 object: exact_object(&format!("{label}/root"), label.as_bytes()),
222 };
223 let slot = |stream: &str| {
224 crate::objects::ObjectSlot::logical(format!("store-v1/test/{label}/{stream}/1.json"))
225 .unwrap()
226 };
227 let registration = store_commit::StoreDeviceRegistration::signed(
228 root.clone(),
229 store_commit::StoreDeviceRegistrationOrigin::Founder {
230 creation_id: store_commit::StoreCreationId::from_nonce(label),
231 },
232 crate::objects::ProviderDeviceBinding {
233 principal: crate::objects::ProviderPrincipalId::CustomS3Credential {
234 access_key_id_hash: ObjectHash::digest(label.as_bytes()),
235 },
236 },
237 store_commit::DeviceStreamAnchor::StoreAcknowledgements {
238 first_slot: slot("acknowledgements"),
239 },
240 identity,
241 )
242 .expect("sign test device registration");
243 let bytes = registration.to_bytes();
244 let reference = store_commit::StoreDeviceRegistrationRef::from_registration(
245 ®istration,
246 exact_object(&format!("{label}/registration"), &bytes),
247 );
248 let device_signer = registration
249 .device_signer(identity)
250 .expect("derive registered device signer");
251 let stream_id = store_commit::StreamActivation::device_authorized_stream_id(
252 root.store_root_hash,
253 &reference,
254 store_commit::StreamAnchorDomain::StoreAnnouncements,
255 );
256 MergeDeviceAuthority {
257 registration,
258 reference,
259 device_signer,
260 stream_id,
261 }
262}