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}