Skip to main content

coven_protocol/membership/
entry.rs

1use 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}