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