1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case", deny_unknown_fields)]
5pub enum StoreDeviceProposalState {
6 Pending {
7 proposal: StoreDeviceExclusionProposalRef,
8 },
9 Cancelled {
10 outcome: StoreDeviceExclusionCancellationRef,
11 },
12 Superseded {
13 proposal: StoreDeviceExclusionProposalRef,
14 terminals: Vec<StoreDeviceExclusionRef>,
15 },
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct ResolvedStoreDeviceState {
21 pub devices: BTreeMap<StoreDeviceId, StoreDeviceRecord>,
22 pub recovery: Vec<OwnerRecoveryCursor>,
23 pub state_hash: ObjectHash,
24}
25
26impl ResolvedStoreDeviceState {
27 pub fn validate_canonical(&self) -> Result<(), StoreProtocolError> {
28 let canonical = Self::from_parts(self.devices.clone(), self.recovery.clone())?;
29 if canonical != *self {
30 return Err(StoreProtocolError::DeviceStateMismatch);
31 }
32 Ok(())
33 }
34
35 pub fn founder(
36 root: &StoreRootRef,
37 founder_registration: StoreDeviceRegistrationRef,
38 founder_pubkey: &str,
39 founder_grant: MembershipGrantId,
40 founder_recovery: &GrantStreamAnchor,
41 ) -> Result<Self, StoreProtocolError> {
42 let cursor = OwnerRecoveryCursor {
43 owner_grant: founder_grant.clone(),
44 position: OwnerRecoveryPosition::BeforeFirst {
45 activation: OwnerRecoveryActivationId::derive(
46 root,
47 founder_pubkey,
48 &founder_grant,
49 founder_recovery,
50 )?,
51 },
52 };
53 let devices = BTreeMap::from([(
54 founder_registration.device_id,
55 StoreDeviceRecord {
56 registration: founder_registration,
57 proposals: BTreeMap::new(),
58 status: StoreDeviceStatus::Active,
59 },
60 )]);
61 Self::from_parts(devices, vec![cursor])
62 }
63
64 pub fn activate_registration(
65 &self,
66 registration: StoreDeviceRegistrationRef,
67 recovery: Option<OwnerRecoveryCursor>,
68 ) -> Result<Self, StoreProtocolError> {
69 if self.devices.contains_key(®istration.device_id) {
70 return Err(StoreProtocolError::DuplicateDeviceRegistration {
71 device_id: registration.device_id.to_string(),
72 });
73 }
74 let mut devices = self.devices.clone();
75 devices.insert(
76 registration.device_id,
77 StoreDeviceRecord {
78 registration,
79 proposals: BTreeMap::new(),
80 status: StoreDeviceStatus::Active,
81 },
82 );
83 let mut cursors = self.recovery.clone();
84 if let Some(cursor) = recovery {
85 if let Some(existing) = cursors
86 .iter_mut()
87 .find(|existing| existing.owner_grant == cursor.owner_grant)
88 {
89 *existing = cursor;
90 } else {
91 cursors.push(cursor);
92 }
93 }
94 Self::from_parts(devices, cursors)
95 }
96
97 pub fn activate_owner_recovery(
98 &self,
99 owner_grant: MembershipGrantId,
100 activation: OwnerRecoveryActivationId,
101 ) -> Result<Self, StoreProtocolError> {
102 if self
103 .recovery
104 .iter()
105 .any(|cursor| cursor.owner_grant == owner_grant)
106 {
107 return Err(StoreProtocolError::OwnerRecoveryMismatch);
108 }
109 let mut recovery = self.recovery.clone();
110 recovery.push(OwnerRecoveryCursor {
111 owner_grant,
112 position: OwnerRecoveryPosition::BeforeFirst { activation },
113 });
114 Self::from_parts(self.devices.clone(), recovery)
115 }
116
117 pub fn preactivate_recovery_author(
118 mut self,
119 commit: &StoreBatchCommit,
120 registrations: &[ActivatedStoreDeviceRegistration],
121 ) -> Result<(Self, Option<StoreDeviceRegistrationRef>), StoreProtocolError> {
122 if commit.device_registrations().len() != registrations.len() {
123 return Err(StoreProtocolError::Malformed(
124 "verified registrations do not cover every activation".to_string(),
125 ));
126 }
127 for (activated, registration) in commit.device_registrations().iter().zip(registrations) {
128 registration.verify_reference(activated)?;
129 if activated.registration == commit.author_registration {
130 if let Some(cursor) = registration.recovery_cursor()? {
131 self =
132 self.activate_registration(activated.registration.clone(), Some(cursor))?;
133 return Ok((self, Some(activated.registration.clone())));
134 }
135 }
136 }
137 Ok((self, None))
138 }
139
140 pub fn apply_verified_lifecycle(
141 mut self,
142 commit: &StoreBatchCommit,
143 registrations: &[ActivatedStoreDeviceRegistration],
144 preactivated: Option<&StoreDeviceRegistrationRef>,
145 owner_recovery: Option<(MembershipGrantId, OwnerRecoveryActivationId)>,
146 ) -> Result<Self, StoreProtocolError> {
147 if commit.device_registrations().len() != registrations.len() {
148 return Err(StoreProtocolError::Malformed(
149 "verified registrations do not cover every activation".to_string(),
150 ));
151 }
152 for (activated, registration) in commit.device_registrations().iter().zip(registrations) {
153 registration.verify_reference(activated)?;
154 if preactivated != Some(&activated.registration) {
155 self = self.activate_registration(
156 activated.registration.clone(),
157 registration.recovery_cursor()?,
158 )?;
159 }
160 }
161 if let Some((grant_id, activation)) = owner_recovery {
162 self = self.activate_owner_recovery(grant_id, activation)?;
163 }
164 Ok(self)
165 }
166
167 pub fn propose_exclusion(
168 &self,
169 reference: StoreDeviceExclusionProposalRef,
170 proposal: &StoreDeviceExclusionProposal,
171 predecessor_ref: &StoreDeviceStateRef,
172 ) -> Result<Self, StoreProtocolError> {
173 reference.verify_proposal(proposal)?;
174 if &proposal.frozen_device_state != predecessor_ref
175 || predecessor_ref.state_hash() != self.state_hash
176 {
177 return Err(StoreProtocolError::DeviceStateMismatch);
178 }
179 let mut devices = self.devices.clone();
180 let record = devices
181 .get_mut(&reference.target.device_id)
182 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
183 if record.registration != reference.target
184 || !matches!(record.status, StoreDeviceStatus::Active)
185 || record.proposals.contains_key(&reference.proposal_id)
186 {
187 return Err(StoreProtocolError::DeviceStateMismatch);
188 }
189 record.proposals.insert(
190 reference.proposal_id,
191 StoreDeviceProposalState::Pending {
192 proposal: reference,
193 },
194 );
195 Self::from_parts(devices, self.recovery.clone())
196 }
197
198 pub fn cancel_exclusion(
199 &self,
200 cancellation: StoreDeviceExclusionCancellationRef,
201 ) -> Result<Self, StoreProtocolError> {
202 let mut devices = self.devices.clone();
203 let record = devices
204 .get_mut(&cancellation.proposal.target.device_id)
205 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
206 let state = record
207 .proposals
208 .get_mut(&cancellation.proposal.proposal_id)
209 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
210 if !matches!(state, StoreDeviceProposalState::Pending { proposal } if proposal == &cancellation.proposal)
211 {
212 return Err(StoreProtocolError::DeviceStateMismatch);
213 }
214 *state = StoreDeviceProposalState::Cancelled {
215 outcome: cancellation,
216 };
217 Self::from_parts(devices, self.recovery.clone())
218 }
219
220 pub fn exclude(
221 &self,
222 exclusion: StoreDeviceExclusionRef,
223 accepted_cut: StoreHistoryCut,
224 ) -> Result<Self, StoreProtocolError> {
225 validate_store_history_cut(&accepted_cut)?;
226 let mut devices = self.devices.clone();
227 let record = devices
228 .get_mut(&exclusion.proposal.target.device_id)
229 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
230 if record.registration != exclusion.proposal.target
231 || !matches!(record.status, StoreDeviceStatus::Active)
232 || !matches!(
233 record.proposals.get(&exclusion.proposal.proposal_id),
234 Some(StoreDeviceProposalState::Pending { proposal }) if proposal == &exclusion.proposal
235 )
236 {
237 return Err(StoreProtocolError::DeviceStateMismatch);
238 }
239 let terminals = vec![exclusion];
240 supersede_pending_proposals(&mut record.proposals, &terminals);
241 record.status = StoreDeviceStatus::Inactive {
242 terminals,
243 accepted_cut,
244 };
245 Self::from_parts(devices, self.recovery.clone())
246 }
247
248 pub fn merge(states: impl IntoIterator<Item = Self>) -> Result<Self, StoreProtocolError> {
249 let mut devices = BTreeMap::new();
250 let mut recovery = BTreeMap::<MembershipGrantId, OwnerRecoveryPosition>::new();
251 for state in states {
252 for (device_id, record) in state.devices {
253 match devices.entry(device_id) {
254 std::collections::btree_map::Entry::Vacant(entry) => {
255 entry.insert(record);
256 }
257 std::collections::btree_map::Entry::Occupied(mut entry) => {
258 if entry.get().registration != record.registration {
259 return Err(StoreProtocolError::DeviceStateMismatch);
260 }
261 let merged_status =
262 merge_device_status(entry.get().status.clone(), record.status)?;
263 let mut merged_proposals = merge_device_proposals(
264 entry.get().proposals.clone(),
265 record.proposals,
266 )?;
267 if let StoreDeviceStatus::Inactive { terminals, .. } = &merged_status {
268 supersede_pending_proposals(&mut merged_proposals, terminals);
269 }
270 entry.get_mut().status = merged_status;
271 entry.get_mut().proposals = merged_proposals;
272 }
273 }
274 }
275 for cursor in state.recovery {
276 match recovery.entry(cursor.owner_grant) {
277 std::collections::btree_map::Entry::Vacant(entry) => {
278 entry.insert(cursor.position);
279 }
280 std::collections::btree_map::Entry::Occupied(mut entry) => {
281 let merged = entry.get().merge(&cursor.position)?;
285 entry.insert(merged);
286 }
287 }
288 }
289 }
290 Self::from_parts(
291 devices,
292 recovery
293 .into_iter()
294 .map(|(owner_grant, position)| OwnerRecoveryCursor {
295 owner_grant,
296 position,
297 })
298 .collect(),
299 )
300 }
301
302 fn from_parts(
303 devices: BTreeMap<StoreDeviceId, StoreDeviceRecord>,
304 mut recovery: Vec<OwnerRecoveryCursor>,
305 ) -> Result<Self, StoreProtocolError> {
306 recovery.sort();
307 validate_recovery_cursors(&recovery)?;
308 validate_store_device_records(&devices)?;
309 let state_hash = ObjectHash::digest(&domain_json(
310 b"coven.store-device-state.v1\0",
311 &(&devices, &recovery),
312 ));
313 Ok(Self {
314 devices,
315 recovery,
316 state_hash,
317 })
318 }
319}
320
321fn supersede_pending_proposals(
322 proposals: &mut BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
323 terminals: &[StoreDeviceExclusionRef],
324) {
325 for state in proposals.values_mut() {
326 if let StoreDeviceProposalState::Pending { proposal } = state {
327 *state = StoreDeviceProposalState::Superseded {
328 proposal: proposal.clone(),
329 terminals: terminals.to_vec(),
330 };
331 }
332 }
333}
334
335fn merge_device_proposals(
336 mut left: BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
337 right: BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
338) -> Result<BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>, StoreProtocolError>
339{
340 for (proposal_id, right_state) in right {
341 match left.entry(proposal_id) {
342 std::collections::btree_map::Entry::Vacant(entry) => {
343 entry.insert(right_state);
344 }
345 std::collections::btree_map::Entry::Occupied(mut entry) => {
346 let merged = merge_device_proposal_state(entry.get().clone(), right_state)?;
347 entry.insert(merged);
348 }
349 }
350 }
351 Ok(left)
352}
353
354fn merge_device_proposal_state(
355 left: StoreDeviceProposalState,
356 right: StoreDeviceProposalState,
357) -> Result<StoreDeviceProposalState, StoreProtocolError> {
358 let left_proposal = match &left {
359 StoreDeviceProposalState::Pending { proposal }
360 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
361 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
362 };
363 let right_proposal = match &right {
364 StoreDeviceProposalState::Pending { proposal }
365 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
366 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
367 };
368 if left_proposal != right_proposal {
369 return Err(StoreProtocolError::DeviceStateMismatch);
370 }
371 match (left, right) {
372 (
373 StoreDeviceProposalState::Pending { proposal },
374 StoreDeviceProposalState::Pending { .. },
375 ) => Ok(StoreDeviceProposalState::Pending { proposal }),
376 (
377 StoreDeviceProposalState::Cancelled { outcome },
378 StoreDeviceProposalState::Cancelled { outcome: other },
379 ) => {
380 if outcome != other {
381 return Err(StoreProtocolError::DeviceStateMismatch);
382 }
383 Ok(StoreDeviceProposalState::Cancelled { outcome })
384 }
385 (StoreDeviceProposalState::Cancelled { outcome }, _)
386 | (_, StoreDeviceProposalState::Cancelled { outcome }) => {
387 Ok(StoreDeviceProposalState::Cancelled { outcome })
388 }
389 (
390 StoreDeviceProposalState::Superseded {
391 proposal,
392 terminals: left,
393 },
394 StoreDeviceProposalState::Superseded {
395 terminals: right, ..
396 },
397 ) => Ok(StoreDeviceProposalState::Superseded {
398 proposal,
399 terminals: merge_terminal_refs(left, right)?,
400 }),
401 (
402 StoreDeviceProposalState::Superseded {
403 proposal,
404 terminals,
405 },
406 _,
407 )
408 | (
409 _,
410 StoreDeviceProposalState::Superseded {
411 proposal,
412 terminals,
413 },
414 ) => Ok(StoreDeviceProposalState::Superseded {
415 proposal,
416 terminals,
417 }),
418 }
419}
420
421pub(crate) fn merge_device_status(
422 left: StoreDeviceStatus,
423 right: StoreDeviceStatus,
424) -> Result<StoreDeviceStatus, StoreProtocolError> {
425 match (left, right) {
426 (StoreDeviceStatus::Active, StoreDeviceStatus::Active) => Ok(StoreDeviceStatus::Active),
427 (
428 StoreDeviceStatus::Inactive {
429 terminals,
430 accepted_cut,
431 },
432 StoreDeviceStatus::Active,
433 )
434 | (
435 StoreDeviceStatus::Active,
436 StoreDeviceStatus::Inactive {
437 terminals,
438 accepted_cut,
439 },
440 ) => Ok(StoreDeviceStatus::Inactive {
441 terminals,
442 accepted_cut,
443 }),
444 (
445 StoreDeviceStatus::Inactive {
446 terminals: left_terminals,
447 accepted_cut: left_cut,
448 },
449 StoreDeviceStatus::Inactive {
450 terminals: right_terminals,
451 accepted_cut: right_cut,
452 },
453 ) => Ok(StoreDeviceStatus::Inactive {
454 terminals: merge_terminal_refs(left_terminals, right_terminals)?,
455 accepted_cut: intersect_terminal_history_cuts(left_cut, right_cut)?,
456 }),
457 }
458}
459
460fn merge_terminal_refs(
461 left: Vec<StoreDeviceExclusionRef>,
462 right: Vec<StoreDeviceExclusionRef>,
463) -> Result<Vec<StoreDeviceExclusionRef>, StoreProtocolError> {
464 let terminals = left
465 .into_iter()
466 .chain(right)
467 .collect::<BTreeSet<_>>()
468 .into_iter()
469 .collect::<Vec<_>>();
470 validate_terminal_refs(&terminals)?;
471 Ok(terminals)
472}
473
474pub(crate) fn merge_history_cuts(
475 left: StoreHistoryCut,
476 right: StoreHistoryCut,
477) -> Result<StoreHistoryCut, StoreProtocolError> {
478 {
479 let StoreHistoryCut(mut left) = left;
480 let StoreHistoryCut(right) = right;
481 for (stream, reference) in right {
482 match left.entry(stream) {
483 std::collections::btree_map::Entry::Vacant(entry) => {
484 entry.insert(reference);
485 }
486 std::collections::btree_map::Entry::Occupied(mut entry) => {
487 let current = entry.get();
488 if reference.coord.sequence() > current.coord.sequence() {
489 entry.insert(reference);
490 } else if reference.coord.sequence() == current.coord.sequence()
491 && reference != *current
492 {
493 return Err(StoreProtocolError::DeviceStateMismatch);
494 }
495 }
496 }
497 }
498 Ok(StoreHistoryCut(left))
499 }
500}
501
502fn intersect_terminal_history_cuts(
503 left: StoreHistoryCut,
504 right: StoreHistoryCut,
505) -> Result<StoreHistoryCut, StoreProtocolError> {
506 {
507 let StoreHistoryCut(left) = left;
508 let StoreHistoryCut(right) = right;
509 let mut intersection = BTreeMap::new();
510 for (stream, left_reference) in left {
511 let Some(right_reference) = right.get(&stream) else {
512 continue;
513 };
514 let left_sequence = left_reference.coord.sequence();
515 let right_sequence = right_reference.coord.sequence();
516 let reference = if left_sequence < right_sequence {
517 left_reference
518 } else if right_sequence < left_sequence {
519 right_reference.clone()
520 } else if left_reference == *right_reference {
521 left_reference
522 } else {
523 return Err(StoreProtocolError::DeviceStateMismatch);
524 };
525 intersection.insert(stream, reference);
526 }
527 Ok(StoreHistoryCut(intersection))
528 }
529}
530
531fn validate_store_device_records(
532 devices: &BTreeMap<StoreDeviceId, StoreDeviceRecord>,
533) -> Result<(), StoreProtocolError> {
534 for (device_id, record) in devices {
535 if record.registration.device_id != *device_id {
536 return Err(StoreProtocolError::DeviceStateMismatch);
537 }
538 for (proposal_id, state) in &record.proposals {
539 let proposal = match state {
540 StoreDeviceProposalState::Pending { proposal }
541 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
542 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
543 };
544 if proposal.proposal_id != *proposal_id {
545 return Err(StoreProtocolError::DeviceStateMismatch);
546 }
547 if proposal.target != record.registration {
548 return Err(StoreProtocolError::DeviceStateMismatch);
549 }
550 if let StoreDeviceProposalState::Superseded { terminals, .. } = state {
551 validate_terminal_refs(terminals)?;
552 }
553 }
554 if let StoreDeviceStatus::Inactive {
555 terminals,
556 accepted_cut,
557 } = &record.status
558 {
559 validate_terminal_refs(terminals)?;
560 validate_store_history_cut(accepted_cut)?;
561 if record
562 .proposals
563 .values()
564 .any(|state| matches!(state, StoreDeviceProposalState::Pending { .. }))
565 {
566 return Err(StoreProtocolError::DeviceStateMismatch);
567 }
568 }
569 }
570 Ok(())
571}
572
573fn validate_terminal_refs(terminals: &[StoreDeviceExclusionRef]) -> Result<(), StoreProtocolError> {
574 if terminals.is_empty() || terminals.windows(2).any(|pair| pair[0] >= pair[1]) {
575 return Err(StoreProtocolError::DeviceStateMismatch);
576 }
577 Ok(())
578}
579
580pub(crate) fn canonical_recovery_cursors(
581 mut recovery: Vec<OwnerRecoveryCursor>,
582) -> Result<Vec<OwnerRecoveryCursor>, StoreProtocolError> {
583 recovery.sort();
584 validate_recovery_cursors(&recovery)?;
585 Ok(recovery)
586}
587
588pub(crate) fn validate_recovery_cursors(
589 recovery: &[OwnerRecoveryCursor],
590) -> Result<(), StoreProtocolError> {
591 if recovery.windows(2).any(|pair| pair[0] >= pair[1]) {
592 return Err(StoreProtocolError::OwnerRecoveryMismatch);
593 }
594 Ok(())
595}