1use super::access::*;
2use super::*;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct VerifiedStreamActivations {
6 activating_commit: StoreBatchCommitRef,
7 activations: Vec<StreamActivation>,
8}
9
10impl VerifiedStreamActivations {
11 pub fn none(
12 commit: &StoreBatchCommit,
13 activating_commit: &StoreBatchCommitRef,
14 ) -> Result<Self, crate::store_commit::StoreProtocolError> {
15 if !commit.stream_activations().is_empty() {
16 return Err(crate::store_commit::StoreProtocolError::Malformed(
17 "Store commit stream activations have not been verified".to_string(),
18 ));
19 }
20 activating_commit.verify_commit(commit)?;
21 Ok(Self {
22 activating_commit: activating_commit.clone(),
23 activations: Vec::new(),
24 })
25 }
26
27 pub fn from_verified_circle_commit(
28 commit: &StoreBatchCommit,
29 activating_commit: &StoreBatchCommitRef,
30 ) -> Result<Self, crate::store_commit::StoreProtocolError> {
31 activating_commit.verify_commit(commit)?;
32 Ok(Self {
33 activating_commit: activating_commit.clone(),
34 activations: commit.stream_activations().to_vec(),
35 })
36 }
37
38 pub(crate) fn from_verified_store_control(
39 commit: &StoreBatchCommit,
40 activating_commit: &StoreBatchCommitRef,
41 ) -> Result<Self, crate::store_commit::StoreProtocolError> {
42 activating_commit.verify_commit(commit)?;
43 if commit.control().is_none() {
44 return Err(crate::store_commit::StoreProtocolError::Malformed(
45 "verified Store membership activations carry another control".to_string(),
46 ));
47 }
48 Ok(Self {
49 activating_commit: activating_commit.clone(),
50 activations: commit.stream_activations().to_vec(),
51 })
52 }
53
54 pub fn as_slice(&self) -> &[StreamActivation] {
55 &self.activations
56 }
57
58 pub fn activating_commit(&self) -> &StoreBatchCommitRef {
59 &self.activating_commit
60 }
61}
62
63#[derive(Debug, Clone)]
64pub struct VerifiedStreamActivationPrefix {
65 by_activation: BTreeMap<StreamActivationId, (StreamActivation, StoreBatchCommitRef)>,
66}
67
68impl VerifiedStreamActivationPrefix {
69 pub fn empty() -> Self {
70 Self {
71 by_activation: BTreeMap::new(),
72 }
73 }
74
75 pub fn activation(
76 &self,
77 activation_id: StreamActivationId,
78 ) -> Option<&(StreamActivation, StoreBatchCommitRef)> {
79 self.by_activation.get(&activation_id)
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct VerifiedCircleActivations {
85 pub(super) circles: Vec<VerifiedCircleReference>,
86 pub(super) stream_activations: VerifiedStreamActivations,
87 pub(super) bootstraps: Vec<VerifiedCircleImage>,
88 pub(super) local_exclusions: Vec<LocalCircleExclusion>,
93 pub(super) bootstrap_pending_exclusions: Vec<LocalCircleExclusion>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101struct RetainedCircleActivations {
102 activating_commit: StoreBatchCommitRef,
103 circles: Vec<RetainedCircleReference>,
104 bootstraps: Vec<VerifiedCircleImage>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109struct RetainedCircleReference {
110 reference: CircleControlRef,
111 circle_id: CircleId,
112 control: PreparedCircleControl,
113 local_access: Option<RetainedCircleAccess>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(deny_unknown_fields)]
118struct RetainedCircleAccess {
119 access: PreparedCircleAccess,
120 state: RetainedCircleAccessState,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case", deny_unknown_fields)]
125enum RetainedCircleAccessState {
126 Active {
127 roster: CircleMaterializedRoster,
128 metadata: CircleMetadata,
129 },
130 Inactive,
131}
132
133impl VerifiedCircleActivations {
134 pub fn from_verified_parts(
135 circles: Vec<VerifiedCircleReference>,
136 stream_activations: VerifiedStreamActivations,
137 bootstraps: Vec<VerifiedCircleImage>,
138 local_exclusions: Vec<LocalCircleExclusion>,
139 bootstrap_pending_exclusions: Vec<LocalCircleExclusion>,
140 ) -> Self {
141 Self {
142 circles,
143 stream_activations,
144 bootstraps,
145 local_exclusions,
146 bootstrap_pending_exclusions,
147 }
148 }
149
150 pub fn none(
151 commit: &StoreBatchCommit,
152 commit_ref: &StoreBatchCommitRef,
153 ) -> Result<Self, crate::store_commit::StoreProtocolError> {
154 Ok(Self {
155 circles: Vec::new(),
156 stream_activations: VerifiedStreamActivations::none(commit, commit_ref)?,
157 bootstraps: Vec::new(),
158 local_exclusions: Vec::new(),
159 bootstrap_pending_exclusions: Vec::new(),
160 })
161 }
162
163 pub fn membership_control(
164 commit: &StoreBatchCommit,
165 commit_ref: &StoreBatchCommitRef,
166 ) -> Result<Self, crate::store_commit::StoreProtocolError> {
167 if !commit.circle_controls().is_empty() {
168 return Err(crate::store_commit::StoreProtocolError::Malformed(
169 "Store membership control also carries Circle controls".to_string(),
170 ));
171 }
172 Ok(Self {
173 circles: Vec::new(),
174 stream_activations: VerifiedStreamActivations::from_verified_store_control(
175 commit, commit_ref,
176 )?,
177 bootstraps: Vec::new(),
178 local_exclusions: Vec::new(),
179 bootstrap_pending_exclusions: Vec::new(),
180 })
181 }
182
183 pub fn circles(&self) -> &[VerifiedCircleReference] {
184 &self.circles
185 }
186
187 pub fn stream_activations(&self) -> &VerifiedStreamActivations {
188 &self.stream_activations
189 }
190
191 pub fn bootstraps(&self) -> &[VerifiedCircleImage] {
192 &self.bootstraps
193 }
194
195 pub fn local_exclusions(&self) -> &[LocalCircleExclusion] {
196 &self.local_exclusions
197 }
198
199 pub fn bootstrap_pending_exclusions(&self) -> &[LocalCircleExclusion] {
200 &self.bootstrap_pending_exclusions
201 }
202
203 pub fn to_retained(&self) -> Result<Vec<u8>, CircleStateError> {
204 let retained = RetainedCircleActivations {
205 activating_commit: self.stream_activations.activating_commit.clone(),
206 circles: self
207 .circles
208 .iter()
209 .map(RetainedCircleReference::from_verified)
210 .collect(),
211 bootstraps: self.bootstraps.clone(),
212 };
213 serde_json::to_vec(&retained).map_err(|source| CircleStateError::Json {
214 operation: "serialize retained Circle activations",
215 source,
216 })
217 }
218
219 pub fn parse_retained_for_verified_commit(
220 bytes: &[u8],
221 verified: &VerifiedStoreBatchCommit,
222 recipient_pubkey: Option<&str>,
223 ) -> Result<Self, CircleStateError> {
224 let commit = verified.value();
225 let commit_ref = verified.reference();
226 let retained: RetainedCircleActivations =
227 serde_json::from_slice(bytes).map_err(|source| CircleStateError::Json {
228 operation: "parse retained Circle activations",
229 source,
230 })?;
231 let canonical = serde_json::to_vec(&retained).map_err(|source| CircleStateError::Json {
232 operation: "serialize parsed retained Circle activations",
233 source,
234 })?;
235 if canonical != bytes {
236 return Err(CircleStateError::Invariant(
237 "retained Circle activation bytes are not canonical".to_string(),
238 ));
239 }
240 if retained.activating_commit != *commit_ref
241 || retained.circles.len() != commit.circle_controls().len()
242 {
243 return Err(CircleStateError::Invariant(
244 "retained Circle activations differ from their exact Store commit".to_string(),
245 ));
246 }
247
248 let circles = retained
249 .circles
250 .into_iter()
251 .zip(commit.circle_controls())
252 .map(|(retained, reference)| {
253 retained.verify_and_open(verified, recipient_pubkey, reference)
254 })
255 .collect::<Result<Vec<_>, _>>()?;
256 let mut expected_bootstraps = BTreeMap::new();
257 for circle in &circles {
258 let Some(access) = circle.local_access.as_ref() else {
259 continue;
260 };
261 let CircleAccessDisposition::Active {
262 bootstrap: Some(reference),
263 ..
264 } = &access.leaf.value.disposition
265 else {
266 continue;
267 };
268 if expected_bootstraps
269 .insert(
270 (circle.circle_id, circle.control.coord.clone()),
271 (&access.leaf.value, reference),
272 )
273 .is_some()
274 {
275 return Err(CircleStateError::Invariant(
276 "retained Circle activations repeat a bootstrap recipient".to_string(),
277 ));
278 }
279 }
280 if retained.bootstraps.len() != expected_bootstraps.len() {
281 return Err(CircleStateError::Invariant(
282 "retained Circle bootstrap set is incomplete".to_string(),
283 ));
284 }
285 for bootstrap in &retained.bootstraps {
286 let (access, reference) = expected_bootstraps
287 .remove(&(bootstrap.circle_id, bootstrap.control.clone()))
288 .ok_or_else(|| {
289 CircleStateError::Invariant(
290 "retained Circle bootstrap has no signed access leaf".to_string(),
291 )
292 })?;
293 if bootstrap.reference != *reference {
294 return Err(CircleStateError::Invariant(
295 "retained Circle bootstrap reference differs from its access leaf".to_string(),
296 ));
297 }
298 bootstrap.verify_for_access(access)?;
299 }
300 Ok(Self {
301 circles,
302 stream_activations: VerifiedStreamActivations::from_verified_circle_commit(
303 commit, commit_ref,
304 )?,
305 bootstraps: retained.bootstraps,
306 local_exclusions: Vec::new(),
307 bootstrap_pending_exclusions: Vec::new(),
308 })
309 }
310
311 #[cfg(any(test, feature = "test-utils"))]
312 pub fn parse_retained(
313 bytes: &[u8],
314 commit: &StoreBatchCommit,
315 commit_ref: &StoreBatchCommitRef,
316 author: &StoreDeviceRegistration,
317 recipient_pubkey: Option<&str>,
318 ) -> Result<Self, CircleStateError> {
319 let verified = VerifiedStoreBatchCommit::parse(
320 &commit.to_bytes(),
321 commit.store_root_hash,
322 commit_ref,
323 author,
324 )?;
325 Self::parse_retained_for_verified_commit(bytes, &verified, recipient_pubkey)
326 }
327}
328
329impl RetainedCircleReference {
330 fn from_verified(verified: &VerifiedCircleReference) -> Self {
331 Self {
332 reference: verified.reference.clone(),
333 circle_id: verified.circle_id,
334 control: verified.control.clone(),
335 local_access: verified
336 .local_access
337 .as_ref()
338 .map(RetainedCircleAccess::from_verified),
339 }
340 }
341
342 fn verify_and_open(
343 self,
344 verified: &VerifiedStoreBatchCommit,
345 recipient_pubkey: Option<&str>,
346 reference: &CircleControlRef,
347 ) -> Result<VerifiedCircleReference, CircleStateError> {
348 let commit = verified.value();
349 if self.reference != *reference || self.circle_id != reference.circle_id() {
350 return Err(CircleStateError::Invariant(
351 "retained Circle reference differs from its exact Store commit".to_string(),
352 ));
353 }
354 verify_control_context_for_verified_commit(reference, &self.control, verified)?;
355 let local_access = self
356 .local_access
357 .map(|access| {
358 access.verify_and_open(commit, reference, &self.control, recipient_pubkey)
359 })
360 .transpose()?;
361 let verified = VerifiedCircleReference {
362 reference: self.reference,
363 circle_id: self.circle_id,
364 control: self.control,
365 local_access,
366 };
367 CircleCurrentState::from_verified(commit.candidate_family(), &verified)?;
368 Ok(verified)
369 }
370}
371
372impl RetainedCircleAccess {
373 fn from_verified(verified: &VerifiedCircleAccess) -> Self {
374 let state = match &verified.active {
375 Some(active) => RetainedCircleAccessState::Active {
376 roster: active.roster.clone(),
377 metadata: active.metadata.clone(),
378 },
379 None => RetainedCircleAccessState::Inactive,
380 };
381 Self {
382 access: PreparedCircleAccess {
383 leaf: verified.leaf.clone(),
384 envelope: verified.envelope.clone(),
385 },
386 state,
387 }
388 }
389
390 fn verify_and_open(
391 self,
392 commit: &StoreBatchCommit,
393 reference: &CircleControlRef,
394 control: &PreparedCircleControl,
395 recipient_pubkey: Option<&str>,
396 ) -> Result<VerifiedCircleAccess, CircleStateError> {
397 if !self.access.leaf.verify_envelope(
398 control,
399 &self.access.envelope,
400 commit.candidate_family(),
401 ) {
402 return Err(CircleStateError::Invariant(
403 "retained Circle access leaf and envelope failed verification".to_string(),
404 ));
405 }
406 if let Some(recipient_pubkey) = recipient_pubkey {
407 if self.access.leaf.value.recipient_pubkey != recipient_pubkey {
408 return Err(CircleStateError::Invariant(
409 "retained Circle access names another local recipient".to_string(),
410 ));
411 }
412 }
413 if !reference
414 .objects()
415 .access
416 .iter()
417 .any(|candidate| retained_access_matches(candidate, &self.access))
418 {
419 return Err(CircleStateError::Invariant(
420 "retained Circle access differs from every exact commit reference".to_string(),
421 ));
422 }
423 let active = match (self.access.leaf.value.disposition.clone(), self.state) {
424 (
425 CircleAccessDisposition::Active { .. },
426 RetainedCircleAccessState::Active { roster, metadata },
427 ) => Some(VerifiedCircleActive { roster, metadata }),
428 (CircleAccessDisposition::Inactive, RetainedCircleAccessState::Inactive) => None,
429 _ => {
430 return Err(CircleStateError::Invariant(
431 "retained Circle access state differs from its signed disposition".to_string(),
432 ));
433 }
434 };
435 Ok(VerifiedCircleAccess {
436 envelope: self.access.envelope,
437 leaf: self.access.leaf,
438 active,
439 })
440 }
441}
442
443fn retained_access_matches(
444 reference: &CircleAccessObjectRef,
445 access: &PreparedCircleAccess,
446) -> bool {
447 reference.envelope.owner_pubkey == access.envelope.owner_pubkey
448 && reference.envelope.recipient_slot == access.envelope.recipient_slot
449 && reference.envelope.control_hash == access.envelope.control_hash
450 && reference.envelope.leaf_id == access.envelope.leaf_id
451 && reference.envelope.leaf_hash == access.envelope.leaf_hash
452 && reference.leaf.owner_pubkey == access.leaf.value.owner_pubkey
453 && reference.leaf.epoch_id == access.leaf.value.epoch_id
454 && reference.leaf.recipient_slot == access.leaf.value.recipient_slot
455 && reference.leaf.leaf_id == access.leaf.value.leaf_id
456 && reference.leaf.leaf_hash == access.leaf.leaf_hash
457 && reference.leaf.object.stored_hash() == access.leaf.leaf_hash
458 && u64::try_from(access.leaf.bytes.len())
459 .is_ok_and(|size| reference.leaf.object.stored_size() == size)
460 && reference.bootstrap
461 == match &access.leaf.value.disposition {
462 crate::circle::CircleAccessDisposition::Active { bootstrap, .. } => {
463 bootstrap.as_ref().map(|bootstrap| bootstrap.image.clone())
464 }
465 crate::circle::CircleAccessDisposition::Inactive => None,
466 }
467}