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