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