Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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(&current).await {
584                Ok(entry) => {
585                    if !entry.in_tree(tree) {
586                        continue;
587                    }
588                    unmet.remove(&current);
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}