Skip to main content

coven_protocol/
circle_test_fixtures.rs

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