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 ) -> Result<Self, StoreProtocolError> {
172 reference.verify_proposal(proposal)?;
173 let record = self
174 .devices
175 .get(&reference.target.device_id)
176 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
177 if record.registration != reference.target
178 || !matches!(record.status, StoreDeviceStatus::Active)
179 || record.proposals.contains_key(&reference.proposal_id)
180 {
181 return Err(StoreProtocolError::DeviceStateMismatch);
182 }
183 Self::merge([
184 self.clone(),
185 Self::exclusion_effect(StoreDeviceProposalState::Pending {
186 proposal: reference,
187 })?,
188 ])
189 }
190
191 pub fn cancel_exclusion(
192 &self,
193 cancellation: StoreDeviceExclusionCancellationRef,
194 ) -> Result<Self, StoreProtocolError> {
195 let record = self
196 .devices
197 .get(&cancellation.proposal.target.device_id)
198 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
199 let state = record
200 .proposals
201 .get(&cancellation.proposal.proposal_id)
202 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
203 if !matches!(state, StoreDeviceProposalState::Pending { proposal } if proposal == &cancellation.proposal)
204 {
205 return Err(StoreProtocolError::DeviceStateMismatch);
206 }
207 Self::merge([
208 self.clone(),
209 Self::exclusion_effect(StoreDeviceProposalState::Cancelled {
210 outcome: cancellation,
211 })?,
212 ])
213 }
214
215 pub fn exclude(&self, exclusion: StoreDeviceExclusionRef) -> Result<Self, StoreProtocolError> {
216 let record = self
217 .devices
218 .get(&exclusion.proposal.target.device_id)
219 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
220 if record.registration != exclusion.proposal.target
221 || !matches!(record.status, StoreDeviceStatus::Active)
222 || !matches!(
223 record.proposals.get(&exclusion.proposal.proposal_id),
224 Some(StoreDeviceProposalState::Pending { proposal }) if proposal == &exclusion.proposal
225 )
226 {
227 return Err(StoreProtocolError::DeviceStateMismatch);
228 }
229 Self::merge([
230 self.clone(),
231 Self::exclusion_effect(StoreDeviceProposalState::Superseded {
232 proposal: exclusion.proposal.clone(),
233 terminals: vec![exclusion],
234 })?,
235 ])
236 }
237
238 pub(super) fn exclusion_effect(
241 effect: StoreDeviceProposalState,
242 ) -> Result<Self, StoreProtocolError> {
243 let (proposal, status) = match &effect {
244 StoreDeviceProposalState::Pending { proposal } => (proposal, StoreDeviceStatus::Active),
245 StoreDeviceProposalState::Cancelled { outcome } => {
246 (&outcome.proposal, StoreDeviceStatus::Active)
247 }
248 StoreDeviceProposalState::Superseded {
249 proposal,
250 terminals,
251 } => (
252 proposal,
253 StoreDeviceStatus::Inactive {
254 terminals: terminals.clone(),
255 },
256 ),
257 };
258 let registration = proposal.target.clone();
259 let proposals = BTreeMap::from([(proposal.proposal_id, effect)]);
260 Self::from_parts(
261 BTreeMap::from([(
262 registration.device_id,
263 StoreDeviceRecord {
264 registration,
265 proposals,
266 status,
267 },
268 )]),
269 Vec::new(),
270 )
271 }
272
273 pub fn merge(states: impl IntoIterator<Item = Self>) -> Result<Self, StoreProtocolError> {
274 let mut devices = BTreeMap::new();
275 let mut recovery = BTreeMap::<MembershipGrantId, OwnerRecoveryPosition>::new();
276 for state in states {
277 for (device_id, record) in state.devices {
278 match devices.entry(device_id) {
279 std::collections::btree_map::Entry::Vacant(entry) => {
280 entry.insert(record);
281 }
282 std::collections::btree_map::Entry::Occupied(mut entry) => {
283 if entry.get().registration != record.registration {
284 return Err(StoreProtocolError::DeviceStateMismatch);
285 }
286 let merged_status =
287 merge_device_status(entry.get().status.clone(), record.status)?;
288 let mut merged_proposals = merge_device_proposals(
289 entry.get().proposals.clone(),
290 record.proposals,
291 )?;
292 if let StoreDeviceStatus::Inactive { terminals, .. } = &merged_status {
293 supersede_pending_proposals(&mut merged_proposals, terminals);
294 }
295 entry.get_mut().status = merged_status;
296 entry.get_mut().proposals = merged_proposals;
297 }
298 }
299 }
300 for cursor in state.recovery {
301 match recovery.entry(cursor.owner_grant) {
302 std::collections::btree_map::Entry::Vacant(entry) => {
303 entry.insert(cursor.position);
304 }
305 std::collections::btree_map::Entry::Occupied(mut entry) => {
306 let merged = entry.get().merge(&cursor.position)?;
310 entry.insert(merged);
311 }
312 }
313 }
314 }
315 Self::from_parts(
316 devices,
317 recovery
318 .into_iter()
319 .map(|(owner_grant, position)| OwnerRecoveryCursor {
320 owner_grant,
321 position,
322 })
323 .collect(),
324 )
325 }
326
327 fn from_parts(
328 devices: BTreeMap<StoreDeviceId, StoreDeviceRecord>,
329 mut recovery: Vec<OwnerRecoveryCursor>,
330 ) -> Result<Self, StoreProtocolError> {
331 recovery.sort();
332 validate_recovery_cursors(&recovery)?;
333 validate_store_device_records(&devices)?;
334 let state_hash = ObjectHash::digest(&domain_json(
335 b"coven.store-device-state.v1\0",
336 &(&devices, &recovery),
337 ));
338 Ok(Self {
339 devices,
340 recovery,
341 state_hash,
342 })
343 }
344}
345
346fn supersede_pending_proposals(
347 proposals: &mut BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
348 terminals: &[StoreDeviceExclusionRef],
349) {
350 for state in proposals.values_mut() {
351 if let StoreDeviceProposalState::Pending { proposal } = state {
352 *state = StoreDeviceProposalState::Superseded {
353 proposal: proposal.clone(),
354 terminals: terminals.to_vec(),
355 };
356 }
357 }
358}
359
360fn merge_device_proposals(
361 mut left: BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
362 right: BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
363) -> Result<BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>, StoreProtocolError>
364{
365 for (proposal_id, right_state) in right {
366 match left.entry(proposal_id) {
367 std::collections::btree_map::Entry::Vacant(entry) => {
368 entry.insert(right_state);
369 }
370 std::collections::btree_map::Entry::Occupied(mut entry) => {
371 let merged = merge_device_proposal_state(entry.get().clone(), right_state)?;
372 entry.insert(merged);
373 }
374 }
375 }
376 Ok(left)
377}
378
379fn merge_device_proposal_state(
380 left: StoreDeviceProposalState,
381 right: StoreDeviceProposalState,
382) -> Result<StoreDeviceProposalState, StoreProtocolError> {
383 let left_proposal = match &left {
384 StoreDeviceProposalState::Pending { proposal }
385 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
386 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
387 };
388 let right_proposal = match &right {
389 StoreDeviceProposalState::Pending { proposal }
390 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
391 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
392 };
393 if left_proposal != right_proposal {
394 return Err(StoreProtocolError::DeviceStateMismatch);
395 }
396 match (left, right) {
397 (
398 StoreDeviceProposalState::Pending { proposal },
399 StoreDeviceProposalState::Pending { .. },
400 ) => Ok(StoreDeviceProposalState::Pending { proposal }),
401 (
402 StoreDeviceProposalState::Cancelled { outcome },
403 StoreDeviceProposalState::Cancelled { outcome: other },
404 ) => {
405 if outcome != other {
406 return Err(StoreProtocolError::DeviceStateMismatch);
407 }
408 Ok(StoreDeviceProposalState::Cancelled { outcome })
409 }
410 (StoreDeviceProposalState::Cancelled { outcome }, _)
411 | (_, StoreDeviceProposalState::Cancelled { outcome }) => {
412 Ok(StoreDeviceProposalState::Cancelled { outcome })
413 }
414 (
415 StoreDeviceProposalState::Superseded {
416 proposal,
417 terminals: left,
418 },
419 StoreDeviceProposalState::Superseded {
420 terminals: right, ..
421 },
422 ) => Ok(StoreDeviceProposalState::Superseded {
423 proposal,
424 terminals: merge_terminal_refs(left, right)?,
425 }),
426 (
427 StoreDeviceProposalState::Superseded {
428 proposal,
429 terminals,
430 },
431 _,
432 )
433 | (
434 _,
435 StoreDeviceProposalState::Superseded {
436 proposal,
437 terminals,
438 },
439 ) => Ok(StoreDeviceProposalState::Superseded {
440 proposal,
441 terminals,
442 }),
443 }
444}
445
446pub(crate) fn merge_device_status(
447 left: StoreDeviceStatus,
448 right: StoreDeviceStatus,
449) -> Result<StoreDeviceStatus, StoreProtocolError> {
450 match (left, right) {
451 (StoreDeviceStatus::Active, StoreDeviceStatus::Active) => Ok(StoreDeviceStatus::Active),
452 (StoreDeviceStatus::Inactive { terminals }, StoreDeviceStatus::Active)
453 | (StoreDeviceStatus::Active, StoreDeviceStatus::Inactive { terminals }) => {
454 Ok(StoreDeviceStatus::Inactive { terminals })
455 }
456 (
457 StoreDeviceStatus::Inactive { terminals: left },
458 StoreDeviceStatus::Inactive { terminals: right },
459 ) => Ok(StoreDeviceStatus::Inactive {
460 terminals: merge_terminal_refs(left, right)?,
461 }),
462 }
463}
464
465fn merge_terminal_refs(
466 left: Vec<StoreDeviceExclusionRef>,
467 right: Vec<StoreDeviceExclusionRef>,
468) -> Result<Vec<StoreDeviceExclusionRef>, StoreProtocolError> {
469 let terminals = left
470 .into_iter()
471 .chain(right)
472 .collect::<BTreeSet<_>>()
473 .into_iter()
474 .collect::<Vec<_>>();
475 validate_terminal_refs(&terminals)?;
476 Ok(terminals)
477}
478
479pub(crate) fn merge_history_cuts(
480 left: StoreHistoryCut,
481 right: StoreHistoryCut,
482) -> Result<StoreHistoryCut, StoreProtocolError> {
483 {
484 let StoreHistoryCut(mut left) = left;
485 let StoreHistoryCut(right) = right;
486 for (stream, reference) in right {
487 match left.entry(stream) {
488 std::collections::btree_map::Entry::Vacant(entry) => {
489 entry.insert(reference);
490 }
491 std::collections::btree_map::Entry::Occupied(mut entry) => {
492 let current = entry.get();
493 if reference.coord.sequence() > current.coord.sequence() {
494 entry.insert(reference);
495 } else if reference.coord.sequence() == current.coord.sequence()
496 && reference != *current
497 {
498 return Err(StoreProtocolError::DeviceStateMismatch);
499 }
500 }
501 }
502 }
503 Ok(StoreHistoryCut(left))
504 }
505}
506
507fn validate_store_device_records(
508 devices: &BTreeMap<StoreDeviceId, StoreDeviceRecord>,
509) -> Result<(), StoreProtocolError> {
510 for (device_id, record) in devices {
511 if record.registration.device_id != *device_id {
512 return Err(StoreProtocolError::DeviceStateMismatch);
513 }
514 for (proposal_id, state) in &record.proposals {
515 let proposal = match state {
516 StoreDeviceProposalState::Pending { proposal }
517 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
518 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
519 };
520 if proposal.proposal_id != *proposal_id {
521 return Err(StoreProtocolError::DeviceStateMismatch);
522 }
523 if proposal.target != record.registration {
524 return Err(StoreProtocolError::DeviceStateMismatch);
525 }
526 if let StoreDeviceProposalState::Superseded { terminals, .. } = state {
527 validate_terminal_refs(terminals)?;
528 }
529 }
530 if let StoreDeviceStatus::Inactive { terminals } = &record.status {
531 validate_terminal_refs(terminals)?;
532 if record
533 .proposals
534 .values()
535 .any(|state| matches!(state, StoreDeviceProposalState::Pending { .. }))
536 {
537 return Err(StoreProtocolError::DeviceStateMismatch);
538 }
539 }
540 }
541 Ok(())
542}
543
544fn validate_terminal_refs(terminals: &[StoreDeviceExclusionRef]) -> Result<(), StoreProtocolError> {
545 if terminals.is_empty() || terminals.windows(2).any(|pair| pair[0] >= pair[1]) {
546 return Err(StoreProtocolError::DeviceStateMismatch);
547 }
548 Ok(())
549}
550
551pub(crate) fn canonical_recovery_cursors(
552 mut recovery: Vec<OwnerRecoveryCursor>,
553) -> Result<Vec<OwnerRecoveryCursor>, StoreProtocolError> {
554 recovery.sort();
555 validate_recovery_cursors(&recovery)?;
556 Ok(recovery)
557}
558
559pub(crate) fn validate_recovery_cursors(
560 recovery: &[OwnerRecoveryCursor],
561) -> Result<(), StoreProtocolError> {
562 if recovery.windows(2).any(|pair| pair[0] >= pair[1]) {
563 return Err(StoreProtocolError::OwnerRecoveryMismatch);
564 }
565 Ok(())
566}