eidetica/backend/mod.rs
1//! Backend implementations for Eidetica storage
2//!
3//! This module provides the core `BackendImpl` trait and various backend implementations
4//! organized by category (database, file, network, cloud).
5//!
6//! The `BackendImpl` trait defines the interface for storing and retrieving `Entry` objects.
7//! This allows the core database logic (`Instance`, `Database`) to be independent of the specific storage mechanism.
8//!
9//! Instance wraps BackendImpl in a `Backend` struct that provides a layer for future development.
10
11use std::any::Any;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 Result,
18 auth::crypto::{PrivateKey, PublicKey},
19 entry::{Entry, ID},
20 snapshot::Snapshot,
21};
22
23/// Trust/visibility scope for a cached CRDT state entry.
24///
25/// Cached materializations are the same kind of data — opaque serialized
26/// CRDT state bytes — regardless of where they came from. They differ only
27/// in *provenance*, which determines who is allowed to see them:
28///
29/// - **Shared**: bytes the daemon computed itself via a local Transaction.
30/// The daemon is the trusted computer; these bytes are good for any user
31/// with read permission on the database. Populated automatically as a
32/// side effect of `Database::get_store_state` and other daemon-side
33/// materialization paths. Encrypted stores never land here (daemon has no
34/// encryptor key — see [`crate::store::PasswordStore`]), so Shared
35/// entries are always plaintext.
36///
37/// - **User(uuid)**: bytes a specific user uploaded over the service wire
38/// via `CacheCrdtState`. The daemon cannot verify the merge result, so
39/// it is scoped to that user only — alice's upload is invisible to bob.
40/// This is where encrypted-store materializations live (the client
41/// decrypts, merges, re-encrypts, and pushes the ciphertext).
42///
43/// On read, the wire handler tries `User(session_user)` first and falls
44/// back to `Shared` on miss — so a remote read of an unencrypted store
45/// benefits from cross-user dedup via the Shared scope, while encrypted
46/// store reads only ever hit User-scoped entries.
47#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
48pub enum CacheScope {
49 /// Daemon-computed; visible to every user with database read permission.
50 Shared,
51 /// Client-uploaded; visible only to the named user.
52 User(String),
53}
54
55impl CacheScope {
56 /// Storage key for the scope — `None` encodes [`Self::Shared`], `Some`
57 /// encodes [`Self::User`]. Useful for backends that need a single
58 /// nullable column or a uniform key prefix (e.g. SQL primary keys,
59 /// Redis key formatting).
60 pub fn storage_key(&self) -> Option<&str> {
61 match self {
62 CacheScope::Shared => None,
63 CacheScope::User(uuid) => Some(uuid.as_str()),
64 }
65 }
66}
67
68/// Persistent public metadata for an Eidetica instance.
69///
70/// This struct consolidates all instance-level state that needs to persist across restarts:
71/// - The device public key (cryptographic identity)
72/// - System database root IDs
73/// - Optional sync database root ID
74///
75/// The presence of `InstanceMetadata` in a backend indicates an initialized instance.
76/// A backend without metadata is treated as uninitialized and may trigger instance creation.
77///
78/// This struct contains only public information and is safe to transmit over the wire
79/// (e.g., to remote clients via RPC). Private key material is stored separately in
80/// [`InstanceSecrets`].
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct InstanceMetadata {
83 /// Device public key - the instance's cryptographic identity.
84 ///
85 /// This is the public half of the device signing key, generated once during instance
86 /// creation and persisted for the lifetime of the instance. Used for identity
87 /// verification and sync peer identification.
88 pub id: PublicKey,
89
90 /// Root ID of the _users system database.
91 ///
92 /// This database tracks user accounts and their associated data.
93 pub users_db: ID,
94
95 /// Root ID of the _databases system database.
96 ///
97 /// This database tracks metadata about all databases in the instance.
98 pub databases_db: ID,
99
100 /// Root ID of the _sync database (None until `enable_sync()` is called).
101 ///
102 /// This database stores all sync-related state.
103 pub sync_db: Option<ID>,
104}
105
106/// Private secrets for an Eidetica instance.
107///
108/// This struct holds the device signing key, which must never be transmitted
109/// over the wire or exposed to remote clients. It is stored separately from
110/// [`InstanceMetadata`] to enforce this boundary.
111// FIXME: Better secrets management everywhere for InstanceSecrets
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct InstanceSecrets {
114 /// Device signing key - the instance's private cryptographic identity.
115 ///
116 /// This key is generated once during instance creation and persists for the lifetime
117 /// of the instance. It is used to sign system database entries and for sync identity.
118 pub(crate) signing_key: PrivateKey,
119}
120
121// Category modules
122pub mod database;
123pub mod errors;
124
125// Re-export main types for easier access
126pub use errors::BackendError;
127
128/// Verdict of a bounded, tree-scoped reachability query
129/// ([`check_targets_reachable_from`](BackendImpl::check_targets_reachable_from)).
130///
131/// The three states exist so a caller can tell a *proven* negative apart from
132/// "not enough local history to decide" — a distinction that matters for a sync
133/// system, where the latter is transient and self-heals once more of the tree
134/// arrives.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum Reachability {
137 /// Every target is an ancestor-or-equal of some `from` entry, proven
138 /// against fully-present history within the height bound.
139 Reachable,
140 /// Proven negative: the region at or above the target floor was fully
141 /// present locally and at least one target is not an ancestor of `from`
142 /// (e.g. a snapshot regression, or a foreign/fabricated tip).
143 Unreachable,
144 /// Undecidable from local history: a target, a `from` tip, or an ancestor
145 /// needed to reach a target is missing locally. `missing` is the set of
146 /// entries whose absence blocked the decision — the caller should sync
147 /// these and re-check. It is the current blocking *frontier*, not
148 /// necessarily the whole gap: fetching it may reveal a further layer. The
149 /// height bound keeps this set small.
150 Indeterminate {
151 /// Entries that must be synced before the query can be decided.
152 missing: Vec<ID>,
153 },
154}
155
156/// Verification status for entries in the backend.
157///
158/// This enum tracks whether an entry has been cryptographically verified
159/// by the higher-level authentication system. The backend stores this status
160/// but does not perform verification itself - that's handled by the Database/Transaction layers.
161///
162/// Only the local validation pass (`Transaction`) may assign `Verified`: it
163/// is the sole code path that has actually checked the entry's signature and
164/// permissions. Anything arriving from outside this node — over the sync
165/// protocol or the service wire — enters as `Unverified` and can only be
166/// promoted later by a local re-verification pass. A peer cannot assert
167/// `Verified` for us; the wire carries no verification status.
168#[derive(
169 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
170)]
171pub enum VerificationStatus {
172 /// Entry has been cryptographically verified as authentic by *this*
173 /// node's local validation pass. The default for locally created and
174 /// signed entries; never assignable from off-node input.
175 #[default]
176 Verified,
177 /// Entry has not yet been verified by this node — received before
178 /// verification could complete (e.g. a delegated/`_settings` tree it
179 /// depends on has not arrived yet). Transient and promotable: a future
180 /// re-verification pass moves it to `Verified` once its pinned
181 /// settings-ancestor set is present. Admitted into state, flagged.
182 Unverified,
183 /// Entry was checked and *definitively* failed verification — invalid
184 /// signature, revoked key, etc. Terminal; never promoted.
185 Failed,
186}
187
188impl VerificationStatus {
189 /// Canonical persistence encoding. The single source of truth for the
190 /// integer stored in the `verification_status` column; all backends use
191 /// this rather than open-coding the mapping.
192 pub fn as_db_int(self) -> i64 {
193 match self {
194 VerificationStatus::Verified => 0,
195 VerificationStatus::Failed => 1,
196 VerificationStatus::Unverified => 2,
197 }
198 }
199
200 /// Inverse of [`as_db_int`](Self::as_db_int). Errors on an unknown code
201 /// instead of silently collapsing it to `Failed` — a stray value means
202 /// storage corruption, not a failed verification.
203 ///
204 /// This codec is intentionally *additively extensible*: a future state
205 /// (e.g. a peer-attested `Trusted`) takes a fresh, never-reused integer.
206 /// Old data keeps decoding; an old reader rejects the new code rather
207 /// than misinterpreting it; and the wire carries no status at all, so
208 /// adding a state is not a protocol change. Source-level it is
209 /// deliberately *not* non-breaking — the `match` arms here and on
210 /// `VerificationStatus` elsewhere are exhaustive so the compiler
211 /// enumerates every site that must consciously handle the new state.
212 pub fn from_db_int(code: i64) -> Result<Self> {
213 match code {
214 0 => Ok(VerificationStatus::Verified),
215 1 => Ok(VerificationStatus::Failed),
216 2 => Ok(VerificationStatus::Unverified),
217 other => Err(BackendError::TreeIntegrityViolation {
218 reason: format!("unknown verification_status code {other} in storage"),
219 }
220 .into()),
221 }
222 }
223}
224
225/// BackendImpl trait abstracting the underlying storage mechanism for Eidetica entries.
226///
227/// This trait defines the essential operations required for storing, retrieving,
228/// and querying entries and their relationships within databases and stores.
229/// Implementations of this trait handle the specifics of how data is persisted
230/// (e.g., in memory, on disk, in a remote database).
231///
232/// Much of the performance-critical logic, particularly concerning tree traversal
233/// and tip calculation, resides within `BackendImpl` implementations, as the optimal
234/// approach often depends heavily on the underlying storage characteristics.
235///
236/// All backend implementations must be `Send` and `Sync` to allow sharing across threads,
237/// and implement `Any` to allow for downcasting if needed.
238///
239/// Instance wraps BackendImpl in a `Backend` struct that provides additional coordination
240/// and will enable future development.
241///
242/// ## Verification Status
243///
244/// The backend stores a verification status for each entry, indicating whether
245/// the entry has been authenticated by the higher-level authentication system.
246/// The backend itself does not perform verification - it only stores the status
247/// set by the calling code (typically Database/Transaction implementations).
248#[async_trait]
249pub trait BackendImpl: Send + Sync + Any {
250 /// Retrieves an entry by its unique content-addressable ID.
251 ///
252 /// # Arguments
253 /// * `id` - The ID of the entry to retrieve.
254 ///
255 /// # Returns
256 /// A `Result` containing the `Entry` if found, or an `Error::NotFound` otherwise.
257 /// Returns an owned copy to support concurrent access with internal synchronization.
258 async fn get(&self, id: &ID) -> Result<Entry>;
259
260 /// Gets the verification status of an entry.
261 ///
262 /// # Arguments
263 /// * `id` - The ID of the entry to check.
264 ///
265 /// # Returns
266 /// A `Result` containing the `VerificationStatus` if the entry exists, or an `Error::NotFound` otherwise.
267 async fn get_verification_status(&self, id: &ID) -> Result<VerificationStatus>;
268
269 /// Stores an entry.
270 ///
271 /// A **new** entry is stored as [`VerificationStatus::Unverified`]. The
272 /// storage API deliberately does **not** accept a verification status: no
273 /// caller may assert that an entry is verified. `Verified` is reached
274 /// only by this node's local validation pass, which stores via `put` and
275 /// then promotes the entry with
276 /// [`update_verification_status`](Self::update_verification_status).
277 ///
278 /// If an entry with the same ID already exists, `put` is a **no-op**:
279 /// entries are content-addressed and immutable, so the content is
280 /// identical, and the existing verification status is left **untouched**.
281 /// A re-`put` therefore never demotes a prior local promotion — routine
282 /// on overlapping/bootstrap sync, where an already-`Verified` entry is
283 /// commonly re-received. Status transitions go only through
284 /// [`update_verification_status`](Self::update_verification_status).
285 ///
286 /// # Arguments
287 /// * `entry` - The `Entry` to store.
288 ///
289 /// # Returns
290 /// A `Result` indicating success or an error during storage.
291 async fn put(&self, entry: Entry) -> Result<()>;
292
293 /// Updates the verification status of an existing entry.
294 ///
295 /// This is the **only** way an entry becomes `Verified`, and it is
296 /// reserved for this node's local validation pass (and a future
297 /// re-verification pass). It is local-only — never reachable over the
298 /// service wire — so a peer can never assert verification for us.
299 ///
300 /// # Arguments
301 /// * `id` - The ID of the entry to update
302 /// * `verification_status` - The new verification status
303 ///
304 /// # Returns
305 /// A `Result` indicating success or `Error::NotFound` if the entry doesn't exist.
306 async fn update_verification_status(
307 &self,
308 id: &ID,
309 verification_status: VerificationStatus,
310 ) -> Result<()>;
311
312 /// Gets all entries with a specific verification status.
313 ///
314 /// This is useful for finding unverified entries that need authentication
315 /// or for security audits.
316 ///
317 /// # Arguments
318 /// * `status` - The verification status to filter by
319 ///
320 /// # Returns
321 /// A `Result` containing a vector of entry IDs with the specified status.
322 async fn get_entries_by_verification_status(
323 &self,
324 status: VerificationStatus,
325 ) -> Result<Vec<ID>>;
326
327 /// Returns the current [`Snapshot`] of `tree` — its sorted, deduplicated
328 /// set of DAG tips.
329 ///
330 /// Tips are entries within `tree` that have no children *within that same
331 /// tree*: an entry is a child of another iff it lists the other entry in
332 /// its `parents` list.
333 ///
334 /// # Arguments
335 /// * `tree` - The root ID of the tree to snapshot.
336 async fn snapshot(&self, tree: &ID) -> Result<Snapshot>;
337
338 /// Returns the snapshot of a specific store within a given tree.
339 ///
340 /// Store tips are entries within the store that have no children *within
341 /// that same store*. An entry is a child of another within a store if it
342 /// lists the other entry in its `store_parents` list for that store name.
343 ///
344 /// # Arguments
345 /// * `tree` - The root ID of the parent tree.
346 /// * `store` - The name of the store for which to find tips.
347 async fn store_snapshot(&self, tree: &ID, store: &str) -> Result<Snapshot>;
348
349 /// Returns the store snapshot as of a specific main-tree snapshot.
350 ///
351 /// Finds all store entries reachable from the boundary's tips, then filters
352 /// to the ones that are tips within the store.
353 ///
354 /// # Arguments
355 /// * `tree` - The root ID of the parent tree.
356 /// * `store` - The name of the store for which to find tips.
357 /// * `main_snapshot` - Snapshot of the parent tree defining the boundary.
358 async fn store_snapshot_at(
359 &self,
360 tree: &ID,
361 store: &str,
362 main_snapshot: &Snapshot,
363 ) -> Result<Snapshot>;
364
365 /// Retrieves the IDs of all top-level root entries stored in the backend.
366 ///
367 /// Top-level roots are entries that are themselves roots of a tree
368 /// (i.e., `entry.is_root()` is true) and are not part of a larger tree structure
369 /// tracked by the backend (conceptually, their `tree.root` field is empty or refers to themselves,
370 /// though the implementation detail might vary). These represent the starting points
371 /// of distinct trees managed by the database.
372 ///
373 /// # Returns
374 /// A `Result` containing a vector of top-level root entry IDs or an error.
375 async fn all_roots(&self) -> Result<Vec<ID>>;
376
377 /// Finds the merge base (common dominator) of the given entry IDs within a store.
378 ///
379 /// The merge base is the lowest ancestor that ALL paths from ALL entries must pass through.
380 /// This is different from the traditional LCA - if there are parallel paths that bypass
381 /// a common ancestor, that ancestor is not the merge base. This is used to determine
382 /// optimal computation boundaries for CRDT state calculation.
383 ///
384 /// # Arguments
385 /// * `tree` - The root ID of the tree
386 /// * `store` - The name of the store context
387 /// * `entry_ids` - The entry IDs to find the merge base for
388 ///
389 /// # Returns
390 /// A `Result` containing the merge base entry ID, or an error if no common ancestor exists
391 async fn find_merge_base(&self, tree: &ID, store: &str, entry_ids: &[ID]) -> Result<ID>;
392
393 /// Collects all entries from the tree root down to the target entry within a store.
394 ///
395 /// This method performs a complete traversal from the tree root to the target entry,
396 /// collecting all entries that are ancestors of the target within the specified store.
397 /// The result includes the tree root and the target entry itself.
398 ///
399 /// # Arguments
400 /// * `tree` - The root ID of the tree
401 /// * `store` - The name of the store context
402 /// * `target_entry` - The target entry to collect ancestors for
403 ///
404 /// # Returns
405 /// A `Result` containing a vector of entry IDs from root to target, sorted by height
406 async fn collect_root_to_target(
407 &self,
408 tree: &ID,
409 store: &str,
410 target_entry: &ID,
411 ) -> Result<Vec<ID>>;
412
413 /// Returns a reference to the backend instance as a dynamic `Any` type.
414 ///
415 /// This allows for downcasting to a concrete backend implementation if necessary,
416 /// enabling access to implementation-specific methods. Use with caution.
417 fn as_any(&self) -> &dyn Any;
418
419 /// Retrieves all entries belonging to a specific tree, sorted topologically.
420 ///
421 /// The entries are sorted primarily by their height (distance from the root)
422 /// and secondarily by their ID to ensure a consistent, deterministic order suitable
423 /// for reconstructing the tree's history.
424 ///
425 /// **Note:** This potentially loads the entire history of the tree. Use cautiously,
426 /// especially with large trees, as it can be memory-intensive.
427 ///
428 /// # Arguments
429 /// * `tree` - The root ID of the tree to retrieve.
430 ///
431 /// # Returns
432 /// A `Result` containing a vector of all `Entry` objects in the tree,
433 /// sorted topologically, or an error.
434 async fn get_tree(&self, tree: &ID) -> Result<Vec<Entry>>;
435
436 /// Retrieves all entries belonging to a specific store within a tree, sorted topologically.
437 ///
438 /// Similar to `get_tree`, but limited to entries that are part of the specified store.
439 /// The entries are sorted primarily by their height within the store (distance
440 /// from the store's initial entry/entries) and secondarily by their ID.
441 ///
442 /// **Note:** This potentially loads the entire history of the store. Use with caution.
443 ///
444 /// # Arguments
445 /// * `tree` - The root ID of the parent tree.
446 /// * `store` - The name of the store to retrieve.
447 ///
448 /// # Returns
449 /// A `Result` containing a vector of all `Entry` objects in the store,
450 /// sorted topologically according to their position within the store, or an error.
451 async fn get_store(&self, tree: &ID, store: &str) -> Result<Vec<Entry>>;
452
453 /// Retrieves all entries belonging to a specific tree up to the given tips, sorted topologically.
454 ///
455 /// Similar to `get_tree`, but only includes entries that are ancestors of the provided tips.
456 /// This allows reading from a specific state of the tree defined by those tips.
457 ///
458 /// # Arguments
459 /// * `tree` - The root ID of the tree to retrieve.
460 /// * `tips` - The tip IDs defining the state to read from.
461 ///
462 /// # Returns
463 /// A `Result` containing a vector of `Entry` objects in the tree up to the given tips,
464 /// sorted topologically, or an error.
465 ///
466 /// # Errors
467 /// - `EntryNotFound` if any tip doesn't exist locally
468 /// - `EntryNotInTree` if any tip belongs to a different tree
469 async fn get_tree_from_tips(&self, tree: &ID, tips: &[ID]) -> Result<Vec<Entry>>;
470
471 /// Within `tree`, decide whether every entry in `targets` is an
472 /// ancestor-or-equal of some entry in `from` — i.e. whether the `from`
473 /// snapshot is at-or-ahead-of the `targets` snapshot.
474 ///
475 /// Returns a three-state [`Reachability`] rather than a bare bool so a
476 /// *proven* negative is distinguishable from "not enough local history to
477 /// decide" (see [`Reachability::Indeterminate`]). This is the cheap
478 /// counterpart to [`get_tree_from_tips`](Self::get_tree_from_tips) when only
479 /// an at-or-ahead-of answer is needed, not the materialised ancestor set.
480 ///
481 /// **Bounded by the target floor.** The walk never descends below the
482 /// minimum target height: an entry below every remaining target cannot be
483 /// one, nor reach one through still-lower parents (parent heights strictly
484 /// decrease). Cost therefore tracks the height gap between `from` and
485 /// `targets` on *both* the reachable and unreachable paths, not the size of
486 /// the tree — which matters because this runs on every delegated-entry
487 /// validation (and re-validation).
488 ///
489 /// **Validation is symmetric.** Both `from` and `targets` are checked to be
490 /// real entries of `tree`. An entry that exists but belongs to another tree
491 /// is a `from`/target integrity violation and errors. An entry that is
492 /// *missing locally* is not a negative: it makes the verdict
493 /// [`Indeterminate`](Reachability::Indeterminate) and is reported in
494 /// `missing`, so a partially-synced history is never mistaken for a
495 /// regression. Membership is *presence in the tree*, not
496 /// `VerificationStatus::Verified`.
497 ///
498 /// A target equal to a `from` entry is reached; an empty `targets` is
499 /// vacuously [`Reachable`](Reachability::Reachable).
500 ///
501 /// # Errors
502 /// - `EntryNotInTree` if any `from` or `targets` entry exists but belongs to
503 /// a different tree
504 ///
505 /// The default implementation performs the walk via [`get`](Self::get); a
506 /// backend may override it with a single-query traversal.
507 async fn check_targets_reachable_from(
508 &self,
509 tree: &ID,
510 from: &[ID],
511 targets: &[ID],
512 ) -> Result<Reachability> {
513 use std::collections::HashSet;
514
515 // An empty `from` snapshot dominates nothing: any required target is
516 // definitively unreachable. This is a *proven* negative, not
517 // Indeterminate — no amount of syncing lets an empty claim catch up to a
518 // non-empty floor, so we never load the targets to decide it.
519 if from.is_empty() && !targets.is_empty() {
520 return Ok(Reachability::Unreachable);
521 }
522
523 // Validate targets and take the height floor. A target that EXISTS but
524 // is foreign is an integrity violation; a MISSING target means we can't
525 // establish the floor, so it becomes a want-list item (Indeterminate),
526 // never a silent negative.
527 let mut unmet: HashSet<ID> = HashSet::with_capacity(targets.len());
528 let mut missing: Vec<ID> = Vec::new();
529 let mut floor_height = u64::MAX;
530 for target in targets {
531 match self.get(target).await {
532 Ok(entry) => {
533 if !entry.in_tree(tree) {
534 return Err(BackendError::EntryNotInTree {
535 entry_id: target.clone(),
536 tree_id: tree.clone(),
537 }
538 .into());
539 }
540 floor_height = floor_height.min(entry.height());
541 unmet.insert(target.clone());
542 }
543 Err(_) => {
544 unmet.insert(target.clone());
545 missing.push(target.clone());
546 }
547 }
548 }
549
550 let mut visited: HashSet<ID> = HashSet::with_capacity(from.len());
551 let mut stack: Vec<ID> = Vec::new();
552
553 // Seed with `from`: a foreign tip is a forgery (error); a missing tip is
554 // a want-list item. Only expand parents of entries above the floor.
555 for tip in from {
556 match self.get(tip).await {
557 Ok(entry) => {
558 if !entry.in_tree(tree) {
559 return Err(BackendError::EntryNotInTree {
560 entry_id: tip.clone(),
561 tree_id: tree.clone(),
562 }
563 .into());
564 }
565 if visited.insert(tip.clone()) {
566 unmet.remove(tip);
567 if entry.height() > floor_height {
568 stack.extend(entry.parents()?);
569 }
570 }
571 }
572 Err(_) => missing.push(tip.clone()),
573 }
574 }
575
576 // Bounded ancestor walk. A foreign ancestor is simply not part of this
577 // tree's history (skip); a missing one blocks the decision (want-list).
578 while !unmet.is_empty() {
579 let Some(current) = stack.pop() else { break };
580 if !visited.insert(current.clone()) {
581 continue;
582 }
583 match self.get(¤t).await {
584 Ok(entry) => {
585 if !entry.in_tree(tree) {
586 continue;
587 }
588 unmet.remove(¤t);
589 if entry.height() > floor_height {
590 stack.extend(entry.parents()?);
591 }
592 }
593 Err(_) => missing.push(current.clone()),
594 }
595 }
596
597 Ok(if unmet.is_empty() {
598 Reachability::Reachable
599 } else if !missing.is_empty() {
600 missing.sort();
601 missing.dedup();
602 Reachability::Indeterminate { missing }
603 } else {
604 Reachability::Unreachable
605 })
606 }
607
608 /// Retrieves all entries belonging to a specific store at the given snapshot, sorted topologically.
609 ///
610 /// Returns entries that are ancestors of the provided store snapshot's tips.
611 ///
612 /// # Arguments
613 /// * `tree` - The root ID of the parent tree.
614 /// * `store` - The name of the store to retrieve.
615 /// * `snapshot` - The store snapshot defining the state to read from.
616 async fn store_at(&self, tree: &ID, store: &str, snapshot: &Snapshot) -> Result<Vec<Entry>>;
617
618 // === CRDT State Cache Methods ===
619 //
620 // These methods provide caching for computed CRDT state at specific
621 // entry+store combinations, scoped by [`CacheScope`]. This optimizes
622 // repeated computations of the same store state from the same set of
623 // tip entries and serves both daemon-local materialization (Shared) and
624 // client-uploaded materialization over the service wire (User).
625
626 /// Get cached CRDT state for a store at a specific entry within a scope.
627 ///
628 /// # Arguments
629 /// * `scope` - Trust scope: [`CacheScope::Shared`] for daemon-computed
630 /// entries (visible to all users), [`CacheScope::User`] for
631 /// client-uploaded entries scoped to that user.
632 /// * `entry_id` - The entry ID where the state is cached.
633 /// * `store` - The name of the store.
634 ///
635 /// # Returns
636 /// A `Result` containing an `Option<Vec<u8>>`. Returns `None` if not cached.
637 /// The bytes are the serialized CRDT state in the store's chosen format
638 /// (plaintext for Shared; ciphertext or plaintext for User, decided
639 /// client-side by the Transaction's encryptor map).
640 async fn get_cached_crdt_state(
641 &self,
642 scope: &CacheScope,
643 entry_id: &ID,
644 store: &str,
645 ) -> Result<Option<Vec<u8>>>;
646
647 /// Cache CRDT state for a store at a specific entry within a scope.
648 ///
649 /// # Arguments
650 /// * `scope` - Trust scope: [`CacheScope::Shared`] for daemon-computed
651 /// entries, [`CacheScope::User`] for client-uploaded entries.
652 /// * `entry_id` - The entry ID where the state should be cached.
653 /// * `store` - The name of the store.
654 /// * `state` - The serialized CRDT state to cache (opaque bytes).
655 ///
656 /// # Returns
657 /// A `Result` indicating success or an error during storage.
658 async fn cache_crdt_state(
659 &self,
660 scope: CacheScope,
661 entry_id: &ID,
662 store: &str,
663 state: Vec<u8>,
664 ) -> Result<()>;
665
666 /// Clear all cached CRDT states.
667 ///
668 /// This is used when the CRDT computation algorithm changes and existing
669 /// cached states may have been computed incorrectly.
670 ///
671 /// # Returns
672 /// A `Result` indicating success or an error during the clear operation.
673 async fn clear_crdt_cache(&self) -> Result<()>;
674
675 /// Get the store parent IDs for a specific entry and store, sorted by height then ID.
676 ///
677 /// This method retrieves the parent entry IDs for a given entry in a specific store
678 /// context, sorted using the same deterministic ordering used throughout the system
679 /// (height ascending, then ID ascending for ties).
680 ///
681 /// # Arguments
682 /// * `tree_id` - The ID of the tree containing the entry
683 /// * `entry_id` - The ID of the entry to get parents for
684 /// * `store` - The name of the store context
685 ///
686 /// # Returns
687 /// A `Result` containing a `Vec<ID>` of parent entry IDs sorted by (height, ID).
688 /// Returns empty vec if the entry has no parents in the store.
689 async fn get_sorted_store_parents(
690 &self,
691 tree_id: &ID,
692 entry_id: &ID,
693 store: &str,
694 ) -> Result<Vec<ID>>;
695
696 /// Gets all entries between one entry and multiple target entries (exclusive of start, inclusive of targets).
697 ///
698 /// This function correctly handles diamond patterns by finding ALL entries that are
699 /// reachable from any of the to_ids by following parents back to from_id, not just single paths.
700 /// The results are deduplicated and sorted by height then ID for deterministic CRDT merge ordering.
701 ///
702 /// # Arguments
703 /// * `tree_id` - The ID of the tree containing the entries
704 /// * `store` - The name of the store context
705 /// * `from_id` - The starting entry ID (not included in result)
706 /// * `to_ids` - The target entry IDs (all included in result)
707 ///
708 /// # Returns
709 /// A `Result<Vec<ID>>` containing all entry IDs between from and any of the targets, deduplicated and sorted by height then ID
710 async fn get_path_from_to(
711 &self,
712 tree_id: &ID,
713 store: &str,
714 from_id: &ID,
715 to_ids: &[ID],
716 ) -> Result<Vec<ID>>;
717
718 // === Instance Metadata Methods ===
719 //
720 // These methods manage persistent instance-level state including the device key
721 // and system database IDs. The presence of metadata indicates an initialized instance.
722
723 /// Get the instance metadata.
724 ///
725 /// Returns `None` for a fresh/uninitialized backend, `Some(metadata)` for an
726 /// initialized instance. This is used during `Instance::open_backend()` to determine
727 /// whether to create a new instance or load an existing one.
728 ///
729 /// # Returns
730 /// A `Result` containing `Option<InstanceMetadata>`:
731 /// - `Some(metadata)` if the instance has been initialized
732 /// - `None` if the backend is fresh/uninitialized
733 async fn get_instance_metadata(&self) -> Result<Option<InstanceMetadata>>;
734
735 /// Set the instance metadata.
736 ///
737 /// This is called during instance creation to persist the device public key and
738 /// system database IDs. It may also be called when enabling sync to update
739 /// the `sync_db` field.
740 ///
741 /// # Arguments
742 /// * `metadata` - The instance metadata to persist
743 ///
744 /// # Returns
745 /// A `Result` indicating success or an error during storage.
746 async fn set_instance_metadata(&self, metadata: &InstanceMetadata) -> Result<()>;
747
748 /// Get the instance secrets (private key material).
749 ///
750 /// Returns `None` if no secrets have been saved.
751 async fn get_instance_secrets(&self) -> Result<Option<InstanceSecrets>>;
752
753 /// Set the instance secrets (private key material).
754 ///
755 /// This is called during instance creation to persist the device signing key
756 /// separately from the public metadata.
757 ///
758 /// # Arguments
759 /// * `secrets` - The instance secrets to persist
760 ///
761 /// # Returns
762 /// A `Result` indicating success or an error during storage.
763 async fn set_instance_secrets(&self, secrets: &InstanceSecrets) -> Result<()>;
764}
765
766#[cfg(test)]
767mod verification_status_codec_tests {
768 use super::VerificationStatus;
769
770 /// Every variant round-trips through the persistence codec, the codes
771 /// are the expected stable values, and they are mutually distinct. This
772 /// pins the on-disk contract so a future state must take a *new* code
773 /// rather than renumber an existing one (which would silently
774 /// reinterpret already-stored data).
775 #[test]
776 fn db_int_roundtrip_and_stable_codes() {
777 for v in [
778 VerificationStatus::Verified,
779 VerificationStatus::Unverified,
780 VerificationStatus::Failed,
781 ] {
782 assert_eq!(VerificationStatus::from_db_int(v.as_db_int()).unwrap(), v);
783 }
784 // Stable wire/disk values — changing any of these is a data-format
785 // break, not a refactor.
786 assert_eq!(VerificationStatus::Verified.as_db_int(), 0);
787 assert_eq!(VerificationStatus::Failed.as_db_int(), 1);
788 assert_eq!(VerificationStatus::Unverified.as_db_int(), 2);
789 }
790
791 /// An unknown code (e.g. one a *future* `Trusted` would use, or storage
792 /// corruption) must be rejected, never silently mapped onto an existing
793 /// state. This is what makes adding a state additively safe: an old
794 /// binary fails closed on data it does not understand.
795 #[test]
796 fn unknown_db_int_is_rejected_not_coerced() {
797 for code in [3_i64, 4, 99, -1] {
798 assert!(
799 VerificationStatus::from_db_int(code).is_err(),
800 "code {code} must error, not coerce to an existing state"
801 );
802 }
803 }
804}