coven_protocol/circle_roster/
conflict.rs1use 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#[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;