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(¤t).await {
740 Ok(entry) => {
741 if !entry.in_tree(tree) {
742 continue;
743 }
744 unmet.remove(¤t);
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}