coven_database/store_state.rs
1//! Membership and audience state after one committed sequence prefix.
2use crate::DbError;
3use coven_crypto::MemberPublicKeys;
4use coven_format::admin::{MemberRole, SnapshotId};
5use coven_foundation::id_source::{CircleId, DeviceId, MemberId, StoreId};
6use coven_merge::{Audience, Slot};
7use std::collections::{BTreeMap, BTreeSet};
8
9/// Store state, including removed identities needed to check older writes (§10.4).
10///
11/// Schema versions belong to each audience: the store's raise is under
12/// [`Audience::Store`], and each circle has its own. A raise is absent until a
13/// kept raise selects it, as in §18.3; before one, the audience is at its base
14/// ([`StoreState::reached`]). These are independent of the local app
15/// migration version returned by [`crate::Database::schema_version`].
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub struct StoreState {
18 /// Absent before the first entry creates the store.
19 pub store: Option<StoreIdentity>,
20 /// Every member added by a kept entry, including removed members.
21 pub members: BTreeMap<MemberId, StoreMember>,
22 /// Every public pair a kept creation, addition or member-key replacement
23 /// bound, including pairs of removed members and pairs a later replacement
24 /// retired. An addition of an id not active in its view, and every
25 /// replacement, must share no key with these (§10).
26 pub bound_keys: BTreeSet<MemberPublicKeys>,
27 /// Every device added by a kept entry, including removed devices.
28 pub devices: BTreeMap<DeviceId, StoreDevice>,
29 /// Every circle made by a kept entry, including deleted circles.
30 pub circles: BTreeMap<CircleId, StoreCircle>,
31 /// Deleted circles and their deciding slots; absence means no deletion.
32 pub circle_deletions: BTreeMap<CircleId, Slot>,
33 /// The selected schema raise per audience; creation itself carries no raise.
34 /// [`Self::reached`] gives an audience's version, a circle's base included.
35 pub schema: BTreeMap<Audience, StoreVersion>,
36 /// The last kept reset of each audience (§19.4).
37 pub resets: BTreeMap<Audience, SnapshotId>,
38}
39
40impl StoreState {
41 /// The member, unless a kept removal removed it.
42 pub fn active_member(&self, id: &MemberId) -> Option<&StoreMember> {
43 self.members.get(id).filter(|member| !member.removed)
44 }
45
46 /// Whether `device` is installed and active, and belongs to `member`.
47 pub fn device_acts_for(&self, device: &DeviceId, member: &MemberId) -> bool {
48 self.devices.get(device).is_some_and(|device| device.active() && device.member == *member)
49 }
50
51 /// Whether `id` is a member who has not been removed and holds the admin role.
52 pub fn active_admin(&self, id: &MemberId) -> bool {
53 self.active_member(id).is_some_and(|member| member.role == MemberRole::Admin)
54 }
55
56 /// Whether `member` remains in the store and in `circle`, which exists and
57 /// is not deleted (§15.5).
58 pub fn reads_circle(&self, circle: CircleId, member: &MemberId) -> bool {
59 self.active_member(member).is_some()
60 && !self.circle_deletions.contains_key(&circle)
61 && self.circles.get(&circle).is_some_and(|c| c.members.contains(member))
62 }
63
64 /// Every circle `member` reads under [`Self::reads_circle`]: the circles
65 /// whose parts its devices open, snapshot and reload (§15.6, §16).
66 pub fn readable_circles(&self, member: &MemberId) -> BTreeSet<CircleId> {
67 self.circles.keys().copied().filter(|circle| self.reads_circle(*circle, member)).collect()
68 }
69
70 /// The schema version `audience` has reached: the one its latest kept
71 /// raise selected, or before any raise its base, zero for the store and
72 /// for a circle the breaking version its creation recorded. A raise goes
73 /// past the base, so this is the greater of the two (§16.3, §18.3).
74 pub fn reached(&self, audience: &Audience) -> u32 {
75 self.schema.get(audience).map(|raised| raised.number).unwrap_or_else(|| match audience {
76 Audience::Store => 0,
77 Audience::Circle(circle) => self.circles.get(circle).map_or(0, |circle| circle.base),
78 })
79 }
80
81 /// Whether a part at `version` in `audience` waits for a raise: the
82 /// audience is below that version and can still be raised. Nobody can
83 /// raise a deleted circle, and its parts are taken out wherever they land
84 /// (§15.9, §18.5).
85 pub fn awaits_raise(&self, audience: &Audience, version: u32) -> bool {
86 !self.deleted(audience) && self.reached(audience) < version
87 }
88
89 /// Whether `circle` is one this state doesn't hold, or deleted: a change
90 /// aimed at it meets a gone target (§10.4, §15.9).
91 pub fn gone_circle(&self, circle: &CircleId) -> bool {
92 !self.circles.contains_key(circle) || self.circle_deletions.contains_key(circle)
93 }
94
95 /// Whether `audience` is a deleted circle (§15.9).
96 pub fn deleted(&self, audience: &Audience) -> bool {
97 match audience {
98 Audience::Store => false,
99 Audience::Circle(circle) => self.circle_deletions.contains_key(circle),
100 }
101 }
102
103 /// Every audience `member` reads: the store's, then each of
104 /// [`Self::readable_circles`] in id order.
105 pub fn readable_audiences(&self, member: &MemberId) -> Vec<Audience> {
106 std::iter::once(Audience::Store)
107 .chain(self.readable_circles(member).into_iter().map(Audience::Circle))
108 .collect()
109 }
110}
111
112/// A store's identity and current name (§10).
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct StoreIdentity {
115 /// The store's id.
116 pub id: StoreId,
117 /// Its name.
118 pub name: String,
119 /// The slot that set `name`: creation's slot 1, or the latest applied
120 /// rename that changed it. A saved restore code's name is replaced only
121 /// by one from a later slot (§13.1).
122 pub named_at: Slot,
123}
124
125impl StoreIdentity {
126 /// Opening requires directory settings to match checked creation (§10).
127 pub fn check_settings(
128 &self,
129 settings: &coven_foundation::files::StoreSettings,
130 ) -> Result<(), DbError> {
131 check_creation(self.id, settings.id)
132 }
133}
134
135/// The sequence's store id must match the directory settings (§10).
136pub(crate) fn check_creation(store: StoreId, settings_store: StoreId) -> Result<(), DbError> {
137 if store != settings_store {
138 return Err(DbError::StoreSettingsMismatch { store });
139 }
140 Ok(())
141}
142
143/// A member in the applied prefix, keyed by stable id in [`StoreState::members`].
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct StoreMember {
146 /// The current public pair: the signing key that verifies this member's
147 /// objects in this view and the sealing key copies go to (§10, §12.1).
148 pub keys: MemberPublicKeys,
149 /// Current access after the applied slot. Revocation also reads superseded
150 /// access from the retained administrative objects (§14.2).
151 pub access: coven_format::MemberAccess,
152 /// The member's role, retained when removed.
153 pub role: MemberRole,
154 /// Whether a kept removal removed this member.
155 pub removed: bool,
156}
157
158/// One install, retained after removal or replacement so its older objects
159/// remain attributable (§10.4).
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct StoreDevice {
162 /// The member who added the device.
163 pub member: MemberId,
164 /// The device's name from its addition.
165 pub name: String,
166 /// Whether kept entries removed or replaced it.
167 pub state: DeviceState,
168}
169
170impl StoreDevice {
171 /// Neither removed nor replaced: it may still author objects (§14.1).
172 pub fn active(&self) -> bool {
173 self.state == DeviceState::Active
174 }
175
176 /// Removed, by its own removal or its member's: it stops for good.
177 pub fn removed(&self) -> bool {
178 self.state == DeviceState::Removed
179 }
180}
181
182/// What counted administrative slots did to a device id (E9). Later slots
183/// from a removed or replaced device are refused (§14.1, D6).
184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
185pub enum DeviceState {
186 /// Registered and neither removed nor replaced.
187 Active,
188 /// A kept removal of the device or of its member.
189 Removed,
190 /// A kept add-device entry names this id as the one it replaces.
191 Replaced,
192}
193
194/// A circle and its current membership; deleted circles retain their name.
195/// Its keys come from received introductions, not this membership projection (§12.1).
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct StoreCircle {
198 /// The last kept name.
199 pub name: String,
200 /// Current members; empty after deletion.
201 pub members: BTreeSet<MemberId>,
202 /// The breaking version its creation entry recorded: its schema version
203 /// until a raise of its own goes past it (§15.5, §16.3).
204 pub base: u32,
205}
206
207/// A selected version raise and the snapshot it chose (§18.3).
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub struct StoreVersion {
210 /// The raised schema version.
211 pub number: u32,
212 /// The snapshot in this version.
213 pub snapshot: SnapshotId,
214 /// The kept raise that selected it.
215 pub entry: Slot,
216}
217
218#[cfg(test)]
219#[path = "store_state_tests.rs"]
220mod tests;