coven_protocol/membership/
entry.rs1use super::*;
2
3#[cfg(any(test, feature = "test-utils"))]
4pub fn test_wrapped_key_ref(
5 owner_pubkey: &str,
6 recipient_pubkey: &str,
7 generation: u64,
8 label: &[u8],
9) -> WrappedStoreKeyRef {
10 let wrap_hash = ObjectHash::digest(
11 &[
12 label,
13 owner_pubkey.as_bytes(),
14 recipient_pubkey.as_bytes(),
15 &generation.to_le_bytes(),
16 ]
17 .concat(),
18 );
19 let logical_key =
20 format!("keys/{owner_pubkey}/{recipient_pubkey}/{generation}/{wrap_hash}.json");
21 WrappedStoreKeyRef {
22 owner_pubkey: owner_pubkey.to_string(),
23 recipient_pubkey: recipient_pubkey.to_string(),
24 generation,
25 wrap_hash,
26 object: ExactObjectRef::new(
27 crate::objects::ObjectSlot::logical(logical_key)
28 .expect("test wrapped-key slot is valid"),
29 label.len() as u64,
30 ObjectHash::digest(label),
31 ),
32 }
33}
34
35pub fn derive_founder_stream_id(store_id: &str, owner_pubkey: &str) -> AuthorStreamId {
36 AuthorStreamId::from_digest(ObjectHash::digest(
37 format!("coven.membership-founder-stream.v1\0{store_id}\0{owner_pubkey}").as_bytes(),
38 ))
39}
40
41pub(super) fn store_membership_anchor_stream(
42 owner_pubkey: &str,
43 owner_grant: &MembershipGrantId,
44 anchor: &GrantStreamAnchor,
45) -> Option<AuthorStreamId> {
46 let GrantStreamAnchor::StoreMembership { first_slot } = anchor else {
47 return None;
48 };
49 let prefix = format!(
50 "{}{owner_pubkey}/{owner_grant}/",
51 crate::store_commit::STORE_MEMBERSHIP_HEAD_PREFIX,
52 );
53 first_slot
54 .logical_key()
55 .strip_prefix(&prefix)?
56 .strip_suffix("/1.json")?
57 .parse()
58 .ok()
59}
60
61pub fn derive_grant_id(
62 store_id: &str,
63 author_pubkey: &str,
64 author_grant: &MembershipGrantId,
65 stream_id: AuthorStreamId,
66 seq: u64,
67 user_pubkey: &str,
68) -> MembershipGrantId {
69 MembershipGrantId(ObjectHash::digest(
70 format!(
71 "coven.membership-grant.v1\0{store_id}\0{author_pubkey}\0{author_grant}\0{stream_id}\0{seq}\0{user_pubkey}"
72 )
73 .as_bytes(),
74 ))
75}
76
77pub fn founder_entry_for_creation(
78 store_id: &str,
79 creation_id: StoreCreationId,
80 owner: &UserKeypair,
81 owner_grant_id: MembershipGrantId,
82 created_at: &str,
83 membership: GrantStreamAnchor,
84 provider_admin: crate::provider::FounderProviderAdminGrant,
85) -> MembershipEntry {
86 let owner_pubkey = keys::public_key_hex(owner);
87 let stream_id = derive_founder_stream_id(store_id, &owner_pubkey);
88 Signed::sign(
89 MembershipEntryBody {
90 store_id: store_id.to_string(),
91 author_pubkey: owner_pubkey.clone(),
92 author_owner_grant: owner_grant_id.clone(),
93 stream_id,
94 seq: 1,
95 previous_hash: None,
96 dependencies: Vec::new(),
97 resolution_dependencies: Vec::new(),
98 created_at: created_at.to_string(),
99 change: MembershipChange::Founder {
100 creation_id,
101 owner_pubkey,
102 owner_grant_id,
103 membership,
104 provider_admin,
105 },
106 provider_admin: None,
107 },
108 owner,
109 )
110}
111
112#[cfg(any(test, feature = "test-utils"))]
113pub fn founder_entry(
114 store_id: &str,
115 owner: &UserKeypair,
116 owner_grant_id: MembershipGrantId,
117 created_at: &str,
118 membership: GrantStreamAnchor,
119 provider_admin: crate::provider::FounderProviderAdminGrant,
120) -> MembershipEntry {
121 founder_entry_for_creation(
122 store_id,
123 StoreCreationId::from_nonce(store_id),
124 owner,
125 owner_grant_id,
126 created_at,
127 membership,
128 provider_admin,
129 )
130}
131
132pub fn verify_membership_entry(entry: &MembershipEntry) -> bool {
133 let activation_position_is_valid = match &entry.change {
134 MembershipChange::ResolutionActivation { .. } => causal_grants::starts_author_stream(
135 entry.seq,
136 entry.previous_hash,
137 &entry.coord().stream_key(),
138 entry.dependencies.iter().map(MembershipCoord::stream_key),
139 ),
140 _ => true,
141 };
142 activation_position_is_valid
143 && entry
144 .resolution_dependencies
145 .windows(2)
146 .all(|pair| pair[0] < pair[1])
147 && entry.verify_by(&entry.author_pubkey).is_ok()
148}