Skip to main content

coven_protocol/
circle_activation_test_fixtures.rs

1//! The founder Circle activation every Circle test starts from: the signed
2//! protocol values, and the current state they derive.
3
4use 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
18/// The founder Circle every layer's Circle tests are built on: a signed device
19/// registration, a resolved founder membership, and the founder transition
20/// whose verified reference derives the current state. `active` decides whether
21/// this device holds Circle access — an inactive Circle is one it was never
22/// granted.
23pub 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        &registration,
94        exact_object(&format!("{label}/registration"), &registration_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        &registration.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}