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