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