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/// Verification status for entries in the backend.
129///
130/// This enum tracks whether an entry has been cryptographically verified
131/// by the higher-level authentication system. The backend stores this status
132/// but does not perform verification itself - that's handled by the Database/Transaction layers.
133///
134/// Only the local validation pass (`Transaction`) may assign `Verified`: it
135/// is the sole code path that has actually checked the entry's signature and
136/// permissions. Anything arriving from outside this node — over the sync
137/// protocol or the service wire — enters as `Unverified` and can only be
138/// promoted later by a local re-verification pass. A peer cannot assert
139/// `Verified` for us; the wire carries no verification status.
140#[derive(
141    Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
142)]
143pub enum VerificationStatus {
144    /// Entry has been cryptographically verified as authentic by *this*
145    /// node's local validation pass. The default for locally created and
146    /// signed entries; never assignable from off-node input.
147    #[default]
148    Verified,
149    /// Entry has not yet been verified by this node — received before
150    /// verification could complete (e.g. a delegated/`_settings` tree it
151    /// depends on has not arrived yet). Transient and promotable: a future
152    /// re-verification pass moves it to `Verified` once its pinned
153    /// settings-ancestor set is present. Admitted into state, flagged.
154    Unverified,
155    /// Entry was checked and *definitively* failed verification — invalid
156    /// signature, revoked key, etc. Terminal; never promoted.
157    Failed,
158}
159
160impl VerificationStatus {
161    /// Canonical persistence encoding. The single source of truth for the
162    /// integer stored in the `verification_status` column; all backends use
163    /// this rather than open-coding the mapping.
164    pub fn as_db_int(self) -> i64 {
165        match self {
166            VerificationStatus::Verified => 0,
167            VerificationStatus::Failed => 1,
168            VerificationStatus::Unverified => 2,
169        }
170    }
171
172    /// Inverse of [`as_db_int`](Self::as_db_int). Errors on an unknown code
173    /// instead of silently collapsing it to `Failed` — a stray value means
174    /// storage corruption, not a failed verification.
175    ///
176    /// This codec is intentionally *additively extensible*: a future state
177    /// (e.g. a peer-attested `Trusted`) takes a fresh, never-reused integer.
178    /// Old data keeps decoding; an old reader rejects the new code rather
179    /// than misinterpreting it; and the wire carries no status at all, so
180    /// adding a state is not a protocol change. Source-level it is
181    /// deliberately *not* non-breaking — the `match` arms here and on
182    /// `VerificationStatus` elsewhere are exhaustive so the compiler
183    /// enumerates every site that must consciously handle the new state.
184    pub fn from_db_int(code: i64) -> Result<Self> {
185        match code {
186            0 => Ok(VerificationStatus::Verified),
187            1 => Ok(VerificationStatus::Failed),
188            2 => Ok(VerificationStatus::Unverified),
189            other => Err(BackendError::TreeIntegrityViolation {
190                reason: format!("unknown verification_status code {other} in storage"),
191            }
192            .into()),
193        }
194    }
195}
196
197/// BackendImpl trait abstracting the underlying storage mechanism for Eidetica entries.
198///
199/// This trait defines the essential operations required for storing, retrieving,
200/// and querying entries and their relationships within databases and stores.
201/// Implementations of this trait handle the specifics of how data is persisted
202/// (e.g., in memory, on disk, in a remote database).
203///
204/// Much of the performance-critical logic, particularly concerning tree traversal
205/// and tip calculation, resides within `BackendImpl` implementations, as the optimal
206/// approach often depends heavily on the underlying storage characteristics.
207///
208/// All backend implementations must be `Send` and `Sync` to allow sharing across threads,
209/// and implement `Any` to allow for downcasting if needed.
210///
211/// Instance wraps BackendImpl in a `Backend` struct that provides additional coordination
212/// and will enable future development.
213///
214/// ## Verification Status
215///
216/// The backend stores a verification status for each entry, indicating whether
217/// the entry has been authenticated by the higher-level authentication system.
218/// The backend itself does not perform verification - it only stores the status
219/// set by the calling code (typically Database/Transaction implementations).
220#[async_trait]
221pub trait BackendImpl: Send + Sync + Any {
222    /// Retrieves an entry by its unique content-addressable ID.
223    ///
224    /// # Arguments
225    /// * `id` - The ID of the entry to retrieve.
226    ///
227    /// # Returns
228    /// A `Result` containing the `Entry` if found, or an `Error::NotFound` otherwise.
229    /// Returns an owned copy to support concurrent access with internal synchronization.
230    async fn get(&self, id: &ID) -> Result<Entry>;
231
232    /// Gets the verification status of an entry.
233    ///
234    /// # Arguments
235    /// * `id` - The ID of the entry to check.
236    ///
237    /// # Returns
238    /// A `Result` containing the `VerificationStatus` if the entry exists, or an `Error::NotFound` otherwise.
239    async fn get_verification_status(&self, id: &ID) -> Result<VerificationStatus>;
240
241    /// Stores an entry.
242    ///
243    /// A **new** entry is stored as [`VerificationStatus::Unverified`]. The
244    /// storage API deliberately does **not** accept a verification status: no
245    /// caller may assert that an entry is verified. `Verified` is reached
246    /// only by this node's local validation pass, which stores via `put` and
247    /// then promotes the entry with
248    /// [`update_verification_status`](Self::update_verification_status).
249    ///
250    /// If an entry with the same ID already exists, `put` is a **no-op**:
251    /// entries are content-addressed and immutable, so the content is
252    /// identical, and the existing verification status is left **untouched**.
253    /// A re-`put` therefore never demotes a prior local promotion — routine
254    /// on overlapping/bootstrap sync, where an already-`Verified` entry is
255    /// commonly re-received. Status transitions go only through
256    /// [`update_verification_status`](Self::update_verification_status).
257    ///
258    /// # Arguments
259    /// * `entry` - The `Entry` to store.
260    ///
261    /// # Returns
262    /// A `Result` indicating success or an error during storage.
263    async fn put(&self, entry: Entry) -> Result<()>;
264
265    /// Updates the verification status of an existing entry.
266    ///
267    /// This is the **only** way an entry becomes `Verified`, and it is
268    /// reserved for this node's local validation pass (and a future
269    /// re-verification pass). It is local-only — never reachable over the
270    /// service wire — so a peer can never assert verification for us.
271    ///
272    /// # Arguments
273    /// * `id` - The ID of the entry to update
274    /// * `verification_status` - The new verification status
275    ///
276    /// # Returns
277    /// A `Result` indicating success or `Error::NotFound` if the entry doesn't exist.
278    async fn update_verification_status(
279        &self,
280        id: &ID,
281        verification_status: VerificationStatus,
282    ) -> Result<()>;
283
284    /// Gets all entries with a specific verification status.
285    ///
286    /// This is useful for finding unverified entries that need authentication
287    /// or for security audits.
288    ///
289    /// # Arguments
290    /// * `status` - The verification status to filter by
291    ///
292    /// # Returns
293    /// A `Result` containing a vector of entry IDs with the specified status.
294    async fn get_entries_by_verification_status(
295        &self,
296        status: VerificationStatus,
297    ) -> Result<Vec<ID>>;
298
299    /// Returns the current [`Snapshot`] of `tree` — its sorted, deduplicated
300    /// set of DAG tips.
301    ///
302    /// Tips are entries within `tree` that have no children *within that same
303    /// tree*: an entry is a child of another iff it lists the other entry in
304    /// its `parents` list.
305    ///
306    /// # Arguments
307    /// * `tree` - The root ID of the tree to snapshot.
308    async fn snapshot(&self, tree: &ID) -> Result<Snapshot>;
309
310    /// Returns the snapshot of a specific store within a given tree.
311    ///
312    /// Store tips are entries within the store that have no children *within
313    /// that same store*. An entry is a child of another within a store if it
314    /// lists the other entry in its `store_parents` list for that store name.
315    ///
316    /// # Arguments
317    /// * `tree` - The root ID of the parent tree.
318    /// * `store` - The name of the store for which to find tips.
319    async fn store_snapshot(&self, tree: &ID, store: &str) -> Result<Snapshot>;
320
321    /// Returns the store snapshot as of a specific main-tree snapshot.
322    ///
323    /// Finds all store entries reachable from the boundary's tips, then filters
324    /// to the ones that are tips within the store.
325    ///
326    /// # Arguments
327    /// * `tree` - The root ID of the parent tree.
328    /// * `store` - The name of the store for which to find tips.
329    /// * `main_snapshot` - Snapshot of the parent tree defining the boundary.
330    async fn store_snapshot_at(
331        &self,
332        tree: &ID,
333        store: &str,
334        main_snapshot: &Snapshot,
335    ) -> Result<Snapshot>;
336
337    /// Retrieves the IDs of all top-level root entries stored in the backend.
338    ///
339    /// Top-level roots are entries that are themselves roots of a tree
340    /// (i.e., `entry.is_root()` is true) and are not part of a larger tree structure
341    /// tracked by the backend (conceptually, their `tree.root` field is empty or refers to themselves,
342    /// though the implementation detail might vary). These represent the starting points
343    /// of distinct trees managed by the database.
344    ///
345    /// # Returns
346    /// A `Result` containing a vector of top-level root entry IDs or an error.
347    async fn all_roots(&self) -> Result<Vec<ID>>;
348
349    /// Finds the merge base (common dominator) of the given entry IDs within a store.
350    ///
351    /// The merge base is the lowest ancestor that ALL paths from ALL entries must pass through.
352    /// This is different from the traditional LCA - if there are parallel paths that bypass
353    /// a common ancestor, that ancestor is not the merge base. This is used to determine
354    /// optimal computation boundaries for CRDT state calculation.
355    ///
356    /// # Arguments
357    /// * `tree` - The root ID of the tree
358    /// * `store` - The name of the store context
359    /// * `entry_ids` - The entry IDs to find the merge base for
360    ///
361    /// # Returns
362    /// A `Result` containing the merge base entry ID, or an error if no common ancestor exists
363    async fn find_merge_base(&self, tree: &ID, store: &str, entry_ids: &[ID]) -> Result<ID>;
364
365    /// Collects all entries from the tree root down to the target entry within a store.
366    ///
367    /// This method performs a complete traversal from the tree root to the target entry,
368    /// collecting all entries that are ancestors of the target within the specified store.
369    /// The result includes the tree root and the target entry itself.
370    ///
371    /// # Arguments
372    /// * `tree` - The root ID of the tree
373    /// * `store` - The name of the store context
374    /// * `target_entry` - The target entry to collect ancestors for
375    ///
376    /// # Returns
377    /// A `Result` containing a vector of entry IDs from root to target, sorted by height
378    async fn collect_root_to_target(
379        &self,
380        tree: &ID,
381        store: &str,
382        target_entry: &ID,
383    ) -> Result<Vec<ID>>;
384
385    /// Returns a reference to the backend instance as a dynamic `Any` type.
386    ///
387    /// This allows for downcasting to a concrete backend implementation if necessary,
388    /// enabling access to implementation-specific methods. Use with caution.
389    fn as_any(&self) -> &dyn Any;
390
391    /// Retrieves all entries belonging to a specific tree, sorted topologically.
392    ///
393    /// The entries are sorted primarily by their height (distance from the root)
394    /// and secondarily by their ID to ensure a consistent, deterministic order suitable
395    /// for reconstructing the tree's history.
396    ///
397    /// **Note:** This potentially loads the entire history of the tree. Use cautiously,
398    /// especially with large trees, as it can be memory-intensive.
399    ///
400    /// # Arguments
401    /// * `tree` - The root ID of the tree to retrieve.
402    ///
403    /// # Returns
404    /// A `Result` containing a vector of all `Entry` objects in the tree,
405    /// sorted topologically, or an error.
406    async fn get_tree(&self, tree: &ID) -> Result<Vec<Entry>>;
407
408    /// Retrieves all entries belonging to a specific store within a tree, sorted topologically.
409    ///
410    /// Similar to `get_tree`, but limited to entries that are part of the specified store.
411    /// The entries are sorted primarily by their height within the store (distance
412    /// from the store's initial entry/entries) and secondarily by their ID.
413    ///
414    /// **Note:** This potentially loads the entire history of the store. Use with caution.
415    ///
416    /// # Arguments
417    /// * `tree` - The root ID of the parent tree.
418    /// * `store` - The name of the store to retrieve.
419    ///
420    /// # Returns
421    /// A `Result` containing a vector of all `Entry` objects in the store,
422    /// sorted topologically according to their position within the store, or an error.
423    async fn get_store(&self, tree: &ID, store: &str) -> Result<Vec<Entry>>;
424
425    /// Retrieves all entries belonging to a specific tree up to the given tips, sorted topologically.
426    ///
427    /// Similar to `get_tree`, but only includes entries that are ancestors of the provided tips.
428    /// This allows reading from a specific state of the tree defined by those tips.
429    ///
430    /// # Arguments
431    /// * `tree` - The root ID of the tree to retrieve.
432    /// * `tips` - The tip IDs defining the state to read from.
433    ///
434    /// # Returns
435    /// A `Result` containing a vector of `Entry` objects in the tree up to the given tips,
436    /// sorted topologically, or an error.
437    ///
438    /// # Errors
439    /// - `EntryNotFound` if any tip doesn't exist locally
440    /// - `EntryNotInTree` if any tip belongs to a different tree
441    async fn get_tree_from_tips(&self, tree: &ID, tips: &[ID]) -> Result<Vec<Entry>>;
442
443    /// Retrieves all entries belonging to a specific store at the given snapshot, sorted topologically.
444    ///
445    /// Returns entries that are ancestors of the provided store snapshot's tips.
446    ///
447    /// # Arguments
448    /// * `tree` - The root ID of the parent tree.
449    /// * `store` - The name of the store to retrieve.
450    /// * `snapshot` - The store snapshot defining the state to read from.
451    async fn store_at(&self, tree: &ID, store: &str, snapshot: &Snapshot) -> Result<Vec<Entry>>;
452
453    // === CRDT State Cache Methods ===
454    //
455    // These methods provide caching for computed CRDT state at specific
456    // entry+store combinations, scoped by [`CacheScope`]. This optimizes
457    // repeated computations of the same store state from the same set of
458    // tip entries and serves both daemon-local materialization (Shared) and
459    // client-uploaded materialization over the service wire (User).
460
461    /// Get cached CRDT state for a store at a specific entry within a scope.
462    ///
463    /// # Arguments
464    /// * `scope` - Trust scope: [`CacheScope::Shared`] for daemon-computed
465    ///   entries (visible to all users), [`CacheScope::User`] for
466    ///   client-uploaded entries scoped to that user.
467    /// * `entry_id` - The entry ID where the state is cached.
468    /// * `store` - The name of the store.
469    ///
470    /// # Returns
471    /// A `Result` containing an `Option<Vec<u8>>`. Returns `None` if not cached.
472    /// The bytes are the serialized CRDT state in the store's chosen format
473    /// (plaintext for Shared; ciphertext or plaintext for User, decided
474    /// client-side by the Transaction's encryptor map).
475    async fn get_cached_crdt_state(
476        &self,
477        scope: &CacheScope,
478        entry_id: &ID,
479        store: &str,
480    ) -> Result<Option<Vec<u8>>>;
481
482    /// Cache CRDT state for a store at a specific entry within a scope.
483    ///
484    /// # Arguments
485    /// * `scope` - Trust scope: [`CacheScope::Shared`] for daemon-computed
486    ///   entries, [`CacheScope::User`] for client-uploaded entries.
487    /// * `entry_id` - The entry ID where the state should be cached.
488    /// * `store` - The name of the store.
489    /// * `state` - The serialized CRDT state to cache (opaque bytes).
490    ///
491    /// # Returns
492    /// A `Result` indicating success or an error during storage.
493    async fn cache_crdt_state(
494        &self,
495        scope: CacheScope,
496        entry_id: &ID,
497        store: &str,
498        state: Vec<u8>,
499    ) -> Result<()>;
500
501    /// Clear all cached CRDT states.
502    ///
503    /// This is used when the CRDT computation algorithm changes and existing
504    /// cached states may have been computed incorrectly.
505    ///
506    /// # Returns
507    /// A `Result` indicating success or an error during the clear operation.
508    async fn clear_crdt_cache(&self) -> Result<()>;
509
510    /// Get the store parent IDs for a specific entry and store, sorted by height then ID.
511    ///
512    /// This method retrieves the parent entry IDs for a given entry in a specific store
513    /// context, sorted using the same deterministic ordering used throughout the system
514    /// (height ascending, then ID ascending for ties).
515    ///
516    /// # Arguments
517    /// * `tree_id` - The ID of the tree containing the entry
518    /// * `entry_id` - The ID of the entry to get parents for
519    /// * `store` - The name of the store context
520    ///
521    /// # Returns
522    /// A `Result` containing a `Vec<ID>` of parent entry IDs sorted by (height, ID).
523    /// Returns empty vec if the entry has no parents in the store.
524    async fn get_sorted_store_parents(
525        &self,
526        tree_id: &ID,
527        entry_id: &ID,
528        store: &str,
529    ) -> Result<Vec<ID>>;
530
531    /// Gets all entries between one entry and multiple target entries (exclusive of start, inclusive of targets).
532    ///
533    /// This function correctly handles diamond patterns by finding ALL entries that are
534    /// reachable from any of the to_ids by following parents back to from_id, not just single paths.
535    /// The results are deduplicated and sorted by height then ID for deterministic CRDT merge ordering.
536    ///
537    /// # Arguments
538    /// * `tree_id` - The ID of the tree containing the entries
539    /// * `store` - The name of the store context
540    /// * `from_id` - The starting entry ID (not included in result)
541    /// * `to_ids` - The target entry IDs (all included in result)
542    ///
543    /// # Returns
544    /// A `Result<Vec<ID>>` containing all entry IDs between from and any of the targets, deduplicated and sorted by height then ID
545    async fn get_path_from_to(
546        &self,
547        tree_id: &ID,
548        store: &str,
549        from_id: &ID,
550        to_ids: &[ID],
551    ) -> Result<Vec<ID>>;
552
553    // === Instance Metadata Methods ===
554    //
555    // These methods manage persistent instance-level state including the device key
556    // and system database IDs. The presence of metadata indicates an initialized instance.
557
558    /// Get the instance metadata.
559    ///
560    /// Returns `None` for a fresh/uninitialized backend, `Some(metadata)` for an
561    /// initialized instance. This is used during `Instance::open_backend()` to determine
562    /// whether to create a new instance or load an existing one.
563    ///
564    /// # Returns
565    /// A `Result` containing `Option<InstanceMetadata>`:
566    /// - `Some(metadata)` if the instance has been initialized
567    /// - `None` if the backend is fresh/uninitialized
568    async fn get_instance_metadata(&self) -> Result<Option<InstanceMetadata>>;
569
570    /// Set the instance metadata.
571    ///
572    /// This is called during instance creation to persist the device public key and
573    /// system database IDs. It may also be called when enabling sync to update
574    /// the `sync_db` field.
575    ///
576    /// # Arguments
577    /// * `metadata` - The instance metadata to persist
578    ///
579    /// # Returns
580    /// A `Result` indicating success or an error during storage.
581    async fn set_instance_metadata(&self, metadata: &InstanceMetadata) -> Result<()>;
582
583    /// Get the instance secrets (private key material).
584    ///
585    /// Returns `None` if no secrets have been saved.
586    async fn get_instance_secrets(&self) -> Result<Option<InstanceSecrets>>;
587
588    /// Set the instance secrets (private key material).
589    ///
590    /// This is called during instance creation to persist the device signing key
591    /// separately from the public metadata.
592    ///
593    /// # Arguments
594    /// * `secrets` - The instance secrets to persist
595    ///
596    /// # Returns
597    /// A `Result` indicating success or an error during storage.
598    async fn set_instance_secrets(&self, secrets: &InstanceSecrets) -> Result<()>;
599}
600
601#[cfg(test)]
602mod verification_status_codec_tests {
603    use super::VerificationStatus;
604
605    /// Every variant round-trips through the persistence codec, the codes
606    /// are the expected stable values, and they are mutually distinct. This
607    /// pins the on-disk contract so a future state must take a *new* code
608    /// rather than renumber an existing one (which would silently
609    /// reinterpret already-stored data).
610    #[test]
611    fn db_int_roundtrip_and_stable_codes() {
612        for v in [
613            VerificationStatus::Verified,
614            VerificationStatus::Unverified,
615            VerificationStatus::Failed,
616        ] {
617            assert_eq!(VerificationStatus::from_db_int(v.as_db_int()).unwrap(), v);
618        }
619        // Stable wire/disk values — changing any of these is a data-format
620        // break, not a refactor.
621        assert_eq!(VerificationStatus::Verified.as_db_int(), 0);
622        assert_eq!(VerificationStatus::Failed.as_db_int(), 1);
623        assert_eq!(VerificationStatus::Unverified.as_db_int(), 2);
624    }
625
626    /// An unknown code (e.g. one a *future* `Trusted` would use, or storage
627    /// corruption) must be rejected, never silently mapped onto an existing
628    /// state. This is what makes adding a state additively safe: an old
629    /// binary fails closed on data it does not understand.
630    #[test]
631    fn unknown_db_int_is_rejected_not_coerced() {
632        for code in [3_i64, 4, 99, -1] {
633            assert!(
634                VerificationStatus::from_db_int(code).is_err(),
635                "code {code} must error, not coerce to an existing state"
636            );
637        }
638    }
639}