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::{
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        &registration,
106        exact_object(&format!("{label}/registration"), &registration_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        &registration.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}