1use crate::circle_activation::{CircleCurrentState, VerifiedCircleReference};
5use coven_keys::keys::{UserKeypair, SIGN_SECRETKEYBYTES};
6
7pub fn test_circle_owner_keypair() -> UserKeypair {
8 let keypair_bytes: [u8; SIGN_SECRETKEYBYTES] = hex::decode(concat!(
9 "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
10 "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"
11 ))
12 .expect("fixed Circle signing key is hexadecimal")
13 .try_into()
14 .expect("fixed Circle signing key is 64 bytes");
15 UserKeypair::from_signing_key_bytes(&keypair_bytes).expect("fixed Circle signing key is valid")
16}
17
18pub struct TestCircleActivation {
24 pub circle_id: crate::circle::CircleId,
25 pub control: crate::circle::PreparedCircleControl,
26 pub owner_pubkey: String,
27 pub current: CircleCurrentState,
28}
29
30pub fn test_circle_activation(label: &str, active: bool) -> TestCircleActivation {
31 use std::collections::BTreeMap;
32
33 use crate::circle::{
34 CircleMetadataHead, CircleRole, CircleRosterDraftPolicy, CircleRosterHead,
35 CircleRosterPolicyObjects, CircleTransitionDraft, CircleTransitionPolicyObjects,
36 PreparedCircleTransition, StoreMembershipStateRef,
37 };
38 use crate::circle_activation::{VerifiedCircleAccess, VerifiedCircleActive};
39 use crate::membership::{
40 MemberRole, MembershipChain, MembershipGrantCreationAuthority, MembershipHeadRef,
41 MembershipStatus,
42 };
43 use crate::objects::ExactObjectRef;
44 use crate::objects::ObjectSlot;
45 use crate::store_commit::{
46 CandidateFamilyId, CircleActivationObjects, CircleMetadataObjectRef, DeviceStreamAnchor,
47 GrantStreamAnchor, ObjectHash, StoreCreationId, StoreDeviceRegistration,
48 StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreRootRef, StreamActivation,
49 SuccessorLink,
50 };
51
52 fn exact_object(label: &str, bytes: &[u8]) -> ExactObjectRef {
53 ExactObjectRef::new(
54 ObjectSlot::logical(format!("store-v1/test/{label}.json"))
55 .expect("valid test object slot"),
56 bytes.len() as u64,
57 ObjectHash::digest(bytes),
58 )
59 }
60
61 let owner = test_circle_owner_keypair();
62 let owner_pubkey = coven_keys::keys::public_key_hex(&owner);
63 let store_root_hash = ObjectHash::digest(format!("{label} Store root").as_bytes());
64 let root_bytes = format!("{label} root").into_bytes();
65 let root = StoreRootRef {
66 store_root_id: ObjectHash::digest(format!("{label} identity").as_bytes()),
67 store_root_hash,
68 object: exact_object(&format!("{label}/root"), &root_bytes),
69 };
70 let registration_origin = StoreDeviceRegistrationOrigin::Founder {
71 creation_id: StoreCreationId::from_random_bytes(
72 *ObjectHash::digest(label.as_bytes()).as_bytes(),
73 ),
74 };
75 let store_commits = DeviceStreamAnchor::StoreAnnouncements {
76 first_slot: ObjectSlot::logical(format!("store-v1/test/{label}/announcements/1.json"))
77 .expect("valid test Store announcement slot"),
78 };
79 let registration = StoreDeviceRegistration::signed(
80 root.clone(),
81 registration_origin,
82 crate::objects::ProviderDeviceBinding {
83 principal: crate::objects::ProviderPrincipalId::CustomS3Credential {
84 access_key_id_hash: ObjectHash::digest(
85 format!("{label} registration access key").as_bytes(),
86 ),
87 },
88 },
89 store_commits,
90 DeviceStreamAnchor::StoreAcknowledgements {
91 first_slot: ObjectSlot::logical(format!(
92 "store-v1/test/{label}/acknowledgements/1.json"
93 ))
94 .expect("valid test Store acknowledgement slot"),
95 },
96 DeviceStreamAnchor::StoreSnapshots {
97 first_slot: ObjectSlot::logical(format!("store-v1/test/{label}/snapshots/1.json"))
98 .expect("valid test Store snapshot slot"),
99 },
100 &owner,
101 )
102 .expect("sign test Store device registration");
103 let registration_bytes = registration.to_bytes();
104 let author_registration = StoreDeviceRegistrationRef::from_registration(
105 ®istration,
106 exact_object(&format!("{label}/registration"), ®istration_bytes),
107 );
108 let device_signer = registration
109 .device_signer(&owner)
110 .expect("derive test Store device signer");
111 let membership_anchor = GrantStreamAnchor::StoreMembership {
112 first_slot: ObjectSlot::logical(format!("store-v1/test/{label}/membership/1.json"))
113 .expect("valid test membership slot"),
114 };
115 let founder = crate::membership::founder_entry(
116 label,
117 &owner,
118 crate::causal_grants::MembershipGrantId::from_test_label(label),
119 "founder",
120 membership_anchor,
121 crate::provider::FounderProviderAdminGrant::from_test_label(label),
122 );
123 let founder_coord = founder.coord();
124 let chain =
125 MembershipChain::from_entries(vec![founder.clone()]).expect("found test membership");
126 let MembershipStatus::Resolved(resolved) = chain.status() else {
127 panic!("founder membership must resolve")
128 };
129 let head = MembershipHeadRef {
130 coord: founder_coord.clone(),
131 head_hash: ObjectHash::digest(format!("{label} membership head").as_bytes()),
132 object: exact_object(&format!("{label}/membership-head"), b"test membership head"),
133 };
134 let membership = StoreMembershipStateRef::from_parts(
135 vec![head],
136 Vec::new(),
137 Vec::new(),
138 resolved.state_hash,
139 )
140 .expect("valid test membership reference");
141 let membership_authority = MembershipGrantCreationAuthority::Entry(founder_coord);
142 let candidate_family = CandidateFamilyId::from_hash(ObjectHash::digest(
143 format!("{label} candidate family").as_bytes(),
144 ));
145 let ids = coven_foundation::id_provider::SequentialIdProvider::new(label);
146 let draft = CircleTransitionDraft::founder(
147 store_root_hash,
148 candidate_family,
149 ®istration.device_id.to_string(),
150 label,
151 "0000000001000-0000-test",
152 membership,
153 membership_authority,
154 vec![(owner_pubkey.clone(), MemberRole::Owner)],
155 &ids,
156 &owner,
157 )
158 .expect("construct test Circle");
159 let control_object = exact_object(&format!("{label}/control"), &draft.control.bytes);
160 let metadata_bytes = serde_json::to_vec(&draft.metadata).expect("serialize test metadata");
161 let metadata_object = exact_object(&format!("{label}/metadata"), &metadata_bytes);
162 let mut roster_entries = BTreeMap::new();
163 let mut roster_heads = Vec::new();
164 let metadata_entries = BTreeMap::from([(
165 draft.metadata.coord(),
166 CircleMetadataObjectRef {
167 key_fingerprint: draft.metadata.key_fingerprint,
168 object: metadata_object.clone(),
169 },
170 )]);
171 let mut metadata_heads = Vec::new();
172 let (policy_objects, head_object) = {
173 let CircleRosterDraftPolicy::Founder {
174 entry: roster_entry,
175 } = &draft.policy.roster
176 else {
177 panic!("founder Circle contains a founder roster entry");
178 };
179 let roster_entry = roster_entry.clone();
180 let roster_bytes =
181 serde_json::to_vec(&roster_entry).expect("serialize test Circle roster entry");
182 let roster_object = exact_object(&format!("{label}/roster-entry"), &roster_bytes);
183 roster_entries.insert(roster_entry.coord(), roster_object.clone());
184
185 let roster_head_slot =
186 ObjectSlot::logical(format!("store-v1/test/{label}/circle-roster-head/1.json"))
187 .expect("valid test Circle roster-head slot");
188 let roster_activation = StreamActivation::grant_authorized(
189 store_root_hash,
190 author_registration.clone(),
191 roster_entry.author_owner_grant.clone(),
192 GrantStreamAnchor::CircleRoster {
193 circle_id: draft.circle_id,
194 first_slot: roster_head_slot.clone(),
195 },
196 );
197 let roster_head = CircleRosterHead::signed(
198 &roster_entry,
199 roster_object,
200 SuccessorLink {
201 activation: roster_activation.activation_id(),
202 predecessor: None,
203 next_slot: ObjectSlot::logical(format!(
204 "store-v1/test/{label}/circle-roster-head/2.json"
205 ))
206 .expect("valid next test Circle roster-head slot"),
207 },
208 &device_signer,
209 );
210 let roster_head_bytes =
211 serde_json::to_vec(&roster_head).expect("serialize test Circle roster head");
212 let roster_head_object = ExactObjectRef::new(
213 roster_head_slot,
214 roster_head_bytes.len() as u64,
215 ObjectHash::digest(&roster_head_bytes),
216 );
217 roster_heads.push(crate::circle::CircleRosterHeadRef::from_stored_head(
218 &roster_head,
219 roster_head_object,
220 ));
221
222 let metadata_head_slot =
223 ObjectSlot::logical(format!("store-v1/test/{label}/circle-metadata-head/1.json"))
224 .expect("valid test Circle metadata-head slot");
225 let metadata_activation = StreamActivation::grant_authorized(
226 store_root_hash,
227 author_registration.clone(),
228 draft.metadata.author_owner_grant.clone(),
229 GrantStreamAnchor::CircleMetadata {
230 circle_id: draft.circle_id,
231 first_slot: metadata_head_slot.clone(),
232 },
233 );
234 let metadata_head = CircleMetadataHead::signed(
235 &draft.metadata,
236 metadata_object,
237 SuccessorLink {
238 activation: metadata_activation.activation_id(),
239 predecessor: None,
240 next_slot: ObjectSlot::logical(format!(
241 "store-v1/test/{label}/circle-metadata-head/2.json"
242 ))
243 .expect("valid next test Circle metadata-head slot"),
244 },
245 &device_signer,
246 );
247 let metadata_head_bytes =
248 serde_json::to_vec(&metadata_head).expect("serialize test Circle metadata head");
249 let metadata_head_object = ExactObjectRef::new(
250 metadata_head_slot,
251 metadata_head_bytes.len() as u64,
252 ObjectHash::digest(&metadata_head_bytes),
253 );
254 metadata_heads.push(crate::circle::CircleMetadataHeadRef::from_stored_head(
255 &metadata_head,
256 metadata_head_object,
257 ));
258
259 let control_head_slot =
260 ObjectSlot::logical(format!("store-v1/test/{label}/circle-control-head/1.json"))
261 .expect("valid test Circle control-head slot");
262 let control_activation = StreamActivation::grant_authorized(
263 store_root_hash,
264 author_registration.clone(),
265 draft.metadata.author_owner_grant.clone(),
266 GrantStreamAnchor::CircleControl {
267 circle_id: draft.circle_id,
268 first_slot: control_head_slot.clone(),
269 },
270 );
271 let control_head = crate::circle::CircleControlHead::signed(
272 &draft.control.value,
273 control_object.clone(),
274 SuccessorLink {
275 activation: control_activation.activation_id(),
276 predecessor: None,
277 next_slot: ObjectSlot::logical(format!(
278 "store-v1/test/{label}/circle-control-head/2.json"
279 ))
280 .expect("valid next test Circle control-head slot"),
281 },
282 &device_signer,
283 );
284 let control_head_bytes =
285 serde_json::to_vec(&control_head).expect("serialize test Circle control head");
286 let control_head_object = ExactObjectRef::new(
287 control_head_slot,
288 control_head_bytes.len() as u64,
289 ObjectHash::digest(&control_head_bytes),
290 );
291 (
292 CircleTransitionPolicyObjects {
293 roster: Some(CircleRosterPolicyObjects {
294 entry: roster_entry,
295 head: roster_head,
296 }),
297 metadata_head: Some(metadata_head),
298 control_head,
299 },
300 Some(control_head_object),
301 )
302 };
303 let creation = PreparedCircleTransition {
304 circle_id: draft.circle_id,
305 epoch_id: draft.epoch_id,
306 keyring: draft.keyring,
307 roster: draft.roster,
308 policy_objects,
309 metadata: draft.metadata,
310 close_intent: draft.close_intent,
311 close_outcome: None,
312 close_cancellation: None,
313 access: draft.access,
314 control: draft.control,
315 };
316 let objects = CircleActivationObjects {
317 control: control_object,
318 close_intent: None,
319 close_outcome: None,
320 close_cancellation: None,
321 roster_entries,
322 roster_heads,
323 roster_resolutions: BTreeMap::new(),
324 metadata_entries,
325 metadata_heads,
326 access: Vec::new(),
327 };
328 let reference = creation.control_ref(objects, head_object);
329 let control = creation.control.clone();
330 let own_access = creation
331 .access
332 .iter()
333 .find(|access| access.leaf.value.recipient_pubkey == owner_pubkey)
334 .expect("test Circle owner access");
335 let activation = VerifiedCircleReference {
336 reference,
337 circle_id: creation.circle_id,
338 control: control.clone(),
339 local_access: active.then(|| VerifiedCircleAccess {
340 envelope: own_access.envelope.clone(),
341 leaf: own_access.leaf.clone(),
342 active: Some(VerifiedCircleActive {
343 roster: creation.resolved_roster(),
344 metadata: creation.metadata.clone(),
345 }),
346 }),
347 };
348 let current = CircleCurrentState::from_verified(candidate_family, &activation)
349 .expect("derive test Circle current state");
350 assert_eq!(
351 creation.resolved_roster().members().get(&owner_pubkey),
352 Some(&CircleRole::Owner)
353 );
354 TestCircleActivation {
355 circle_id: creation.circle_id,
356 control,
357 owner_pubkey,
358 current,
359 }
360}