Skip to main content

coven_protocol/circle_roster/
conflict.rs

1use super::reduction::*;
2use super::*;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct ResolvedCircleRoster {
7    pub grants: BTreeMap<MembershipGrantId, GrantState<CircleGrantRecord, CircleGrantRetirement>>,
8    pub state_hash: ObjectHash,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct CircleRosterBranch {
14    pub heads: Vec<CircleRosterHeadRef>,
15    pub effective_frontier: Vec<CircleRosterCoord>,
16    pub grants: BTreeMap<MembershipGrantId, GrantState<CircleGrantRecord, CircleGrantRetirement>>,
17    pub state_hash: ObjectHash,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case", deny_unknown_fields)]
22pub enum CircleRosterConflict {
23    ConcurrentMemberAssignments {
24        conflict_hash: ObjectHash,
25        heads: Vec<CircleRosterHeadRef>,
26        effective_frontier: Vec<CircleRosterCoord>,
27        member_pubkey: String,
28        conflicting_grants: BTreeMap<MembershipGrantId, CircleGrantRecord>,
29        uncontested_grants: BTreeMap<MembershipGrantId, CircleGrantRecord>,
30    },
31    RevocationCycle {
32        conflict_hash: ObjectHash,
33        heads: Vec<CircleRosterHeadRef>,
34        cyclic_sources: Vec<CircleRosterCoord>,
35        involved_owner_grants: BTreeSet<MembershipGrantId>,
36        maximal_valid_branches: Vec<CircleRosterBranch>,
37    },
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case", deny_unknown_fields)]
42pub enum CircleRosterStatus {
43    Resolved(ResolvedCircleRoster),
44    Conflict(CircleRosterConflict),
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct CircleRosterConflictResolutionRef {
50    pub conflict_hash: ObjectHash,
51    pub resolver_pubkey: String,
52    pub resolution_hash: ObjectHash,
53}
54
55/// The wire body of one Circle roster conflict resolution. Every field here is
56/// signed.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct CircleRosterConflictResolutionBody {
60    pub store_root_hash: ObjectHash,
61    pub circle_id: CircleId,
62    pub conflict_hash: ObjectHash,
63    pub conflicting_heads: Vec<CircleRosterHeadRef>,
64    pub retired_owner_grants: BTreeSet<MembershipGrantId>,
65    pub resolver_pubkey: String,
66    pub resolver_branch_heads: Vec<CircleRosterHeadRef>,
67    pub replacement_grant: MembershipGrantId,
68}
69
70impl SignedBody for CircleRosterConflictResolutionBody {
71    const DOMAIN: &'static [u8] = ROSTER_RESOLUTION_DOMAIN;
72}
73
74pub type CircleRosterConflictResolution = Signed<CircleRosterConflictResolutionBody>;
75
76impl CircleRosterConflictResolution {
77    pub fn resolution_hash(&self) -> ObjectHash {
78        self.hash()
79    }
80
81    pub fn resolution_ref(&self) -> CircleRosterConflictResolutionRef {
82        CircleRosterConflictResolutionRef {
83            conflict_hash: self.conflict_hash,
84            resolver_pubkey: self.resolver_pubkey.clone(),
85            resolution_hash: self.resolution_hash(),
86        }
87    }
88
89    pub fn verify_signature(&self) -> bool {
90        self.replacement_grant
91            == derive_circle_resolution_grant(&self.conflict_hash, &self.resolver_pubkey)
92            && self.verify_by(&self.resolver_pubkey).is_ok()
93    }
94
95    pub(crate) fn verify_against(
96        &self,
97        store_root_hash: ObjectHash,
98        circle_id: CircleId,
99        conflict: &CircleRosterConflict,
100    ) -> bool {
101        let CircleRosterConflict::RevocationCycle {
102            conflict_hash,
103            heads,
104            involved_owner_grants,
105            maximal_valid_branches,
106            ..
107        } = conflict
108        else {
109            return false;
110        };
111        let Some(branch) = maximal_valid_branches
112            .iter()
113            .find(|branch| branch.heads == self.resolver_branch_heads)
114        else {
115            return false;
116        };
117        let mut expected_retired = involved_owner_grants.clone();
118        expected_retired.extend(causal_grants::active_grants(&branch.grants).filter_map(
119            |(grant, record)| {
120                (record.member_pubkey == self.resolver_pubkey && record.role == CircleRole::Owner)
121                    .then_some(grant.clone())
122            },
123        ));
124        self.store_root_hash == store_root_hash
125            && self.circle_id == circle_id
126            && self.conflict_hash == *conflict_hash
127            && self.conflicting_heads == *heads
128            && self.retired_owner_grants == expected_retired
129            && self.replacement_grant
130                == derive_circle_resolution_grant(conflict_hash, &self.resolver_pubkey)
131            && causal_grants::active_grants(&branch.grants).any(|(_, record)| {
132                record.member_pubkey == self.resolver_pubkey && record.role == CircleRole::Owner
133            })
134            && self.verify_signature()
135    }
136}
137
138pub fn derive_circle_resolution_grant(
139    conflict_hash: &ObjectHash,
140    resolver_pubkey: &str,
141) -> MembershipGrantId {
142    MembershipGrantId(ObjectHash::digest(
143        format!("coven.circle-roster-resolution-grant.v1\0{conflict_hash}\0{resolver_pubkey}")
144            .as_bytes(),
145    ))
146}
147
148pub fn resolve_circle_roster_conflict(
149    store_root_hash: ObjectHash,
150    circle_id: CircleId,
151    conflict: &CircleRosterConflict,
152    resolutions: &[CircleRosterConflictResolution],
153) -> Result<ResolvedCircleRoster, CircleRosterError> {
154    let CircleRosterConflict::RevocationCycle {
155        maximal_valid_branches,
156        ..
157    } = conflict
158    else {
159        return Err(CircleRosterError::InvalidConflictResolution);
160    };
161    if resolutions.is_empty() {
162        return Err(CircleRosterError::InvalidConflictResolution);
163    }
164    let mut by_resolver = BTreeMap::new();
165    let mut selected_branches = Vec::new();
166    let mut retired_owner_grants = BTreeSet::new();
167    for resolution in resolutions {
168        if !resolution.verify_against(store_root_hash, circle_id, conflict) {
169            return Err(CircleRosterError::InvalidConflictResolution);
170        }
171        let resolution_hash = resolution.resolution_hash();
172        if let Some(existing) =
173            by_resolver.insert(resolution.resolver_pubkey.clone(), resolution_hash)
174        {
175            if existing != resolution_hash {
176                return Err(CircleRosterError::InvalidConflictResolution);
177            }
178            continue;
179        }
180        let branch = maximal_valid_branches
181            .iter()
182            .find(|branch| branch.heads == resolution.resolver_branch_heads)
183            .ok_or(CircleRosterError::InvalidConflictResolution)?;
184        if !selected_branches
185            .iter()
186            .any(|selected: &&CircleRosterBranch| selected.heads == branch.heads)
187        {
188            selected_branches.push(branch);
189        }
190        retired_owner_grants.extend(resolution.retired_owner_grants.iter().cloned());
191    }
192    let mut resolution_retirements = resolutions
193        .iter()
194        .map(|resolution| CircleGrantRetirement::ConflictResolution(resolution.resolution_ref()));
195    let mut resolution_retirements = GrantRetirements::new(
196        resolution_retirements
197            .next()
198            .expect("validated conflict has a resolution"),
199    );
200    resolution_retirements.extend(
201        resolutions.iter().skip(1).map(|resolution| {
202            CircleGrantRetirement::ConflictResolution(resolution.resolution_ref())
203        }),
204    );
205    let mut grants = causal_grants::resolve_conflict_grants(
206        maximal_valid_branches.iter().map(|branch| &branch.grants),
207        selected_branches
208            .iter()
209            .copied()
210            .map(|branch| &branch.grants),
211        &retired_owner_grants,
212        |_| Ok(resolution_retirements.clone()),
213        || CircleRosterError::InvalidConflictResolution,
214    )?;
215    for resolution in resolutions {
216        let retirements = GrantRetirements::new(CircleGrantRetirement::ConflictResolution(
217            resolution.resolution_ref(),
218        ));
219        for retired in &resolution.retired_owner_grants {
220            let record = selected_branches
221                .iter()
222                .find_map(|branch| branch.grants.get(retired).map(GrantState::record))
223                .ok_or(CircleRosterError::InvalidConflictResolution)?
224                .clone();
225            causal_grants::tombstone_conflict_grant(&mut grants, retired, &record, &retirements)
226                .map_err(|()| CircleRosterError::InvalidConflictResolution)?;
227        }
228    }
229    for resolution in resolutions {
230        let record = CircleGrantRecord {
231            member_pubkey: resolution.resolver_pubkey.clone(),
232            role: CircleRole::Owner,
233            creation_authority: CircleGrantCreationAuthority::ConflictResolution(
234                resolution.resolution_ref(),
235            ),
236        };
237        if grants
238            .insert(
239                resolution.replacement_grant.clone(),
240                GrantState::Active {
241                    record: record.clone(),
242                },
243            )
244            .is_some_and(|current| current.active() != Some(&record))
245        {
246            return Err(CircleRosterError::InvalidConflictResolution);
247        }
248    }
249    if !roster_grants_are_valid(&grants) {
250        return Err(CircleRosterError::InvalidConflictResolution);
251    }
252    Ok(ResolvedCircleRoster {
253        state_hash: circle_roster_state_hash(&grants),
254        grants,
255    })
256}
257
258impl ResolvedCircleRoster {
259    pub fn state_hash(&self) -> ObjectHash {
260        self.state_hash
261    }
262
263    pub fn members(&self) -> BTreeMap<String, CircleRole> {
264        roster_members(&self.grants)
265    }
266
267    pub fn authorizes_owner_grant(
268        &self,
269        author_pubkey: &str,
270        grant_id: &MembershipGrantId,
271        created_at: &CircleRosterCoord,
272    ) -> bool {
273        self.authorizes_owner_grant_id(author_pubkey, grant_id)
274            && self
275                .grants
276                .get(grant_id)
277                .and_then(GrantState::active)
278                .is_some_and(|record| {
279                    record.creation_authority
280                        == CircleGrantCreationAuthority::Entry(created_at.clone())
281                })
282    }
283
284    pub fn authorizes_resolution_grant(
285        &self,
286        author_pubkey: &str,
287        grant_id: &MembershipGrantId,
288        resolution: &CircleRosterConflictResolutionRef,
289    ) -> bool {
290        self.authorizes_owner_grant_id(author_pubkey, grant_id)
291            && self
292                .grants
293                .get(grant_id)
294                .and_then(GrantState::active)
295                .is_some_and(|record| {
296                    record.creation_authority
297                        == CircleGrantCreationAuthority::ConflictResolution(resolution.clone())
298                })
299    }
300
301    pub fn authorizes_owner_grant_id(
302        &self,
303        author_pubkey: &str,
304        grant_id: &MembershipGrantId,
305    ) -> bool {
306        roster_authorizes_owner_grant(&self.grants, author_pubkey, grant_id)
307    }
308
309    pub fn verify(&self) -> bool {
310        self.state_hash == circle_roster_state_hash(&self.grants)
311            && roster_grants_are_valid(&self.grants)
312    }
313
314    pub fn active_grants(&self) -> impl Iterator<Item = (&MembershipGrantId, &CircleGrantRecord)> {
315        causal_grants::active_grants(&self.grants)
316    }
317}
318
319#[cfg(any(test, feature = "test-utils"))]
320impl CircleRosterBranch {
321    pub fn active_grants(&self) -> impl Iterator<Item = (&MembershipGrantId, &CircleGrantRecord)> {
322        causal_grants::active_grants(&self.grants)
323    }
324}
325
326pub type CircleMaterializedRoster = ResolvedCircleRoster;