rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

state_maintenance.rs (10915B)


      1 //! RHI-bound integrity, backup, and offline restore integration.
      2 
      3 use core::{fmt, num::NonZeroU64};
      4 use std::{error::Error, path::Path};
      5 
      6 use radroots_service_sqlite::{
      7     BackupManifestSha256, ServiceBackupManifest, ServiceDatabaseMetadata, ServiceSqliteError,
      8     ServiceSqliteErrorKind, StagedServiceRestore, VerifiedServiceBackup, finalize_staged_restore,
      9     stage_verified_restore, verify_backup_bundle,
     10 };
     11 
     12 use crate::{RhiRuntimeContext, RhiStateMetadata, state_host};
     13 
     14 /// Stable source-free class for an RHI state-maintenance failure.
     15 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     16 pub enum RhiStateMaintenanceErrorKind {
     17     InvalidEvidence,
     18     InvalidMode,
     19     Catalog,
     20     Authority,
     21     Open,
     22     Metadata,
     23     Migration,
     24     Backup,
     25     Restore,
     26     Integrity,
     27     Recovery,
     28 }
     29 
     30 impl RhiStateMaintenanceErrorKind {
     31     /// Returns the stable machine-readable failure code.
     32     #[must_use]
     33     pub const fn code(self) -> &'static str {
     34         match self {
     35             Self::InvalidEvidence => "state_maintenance_evidence_invalid",
     36             Self::InvalidMode => "state_maintenance_mode_invalid",
     37             Self::Catalog => "state_maintenance_catalog_invalid",
     38             Self::Authority => "state_maintenance_authority_failed",
     39             Self::Open => "state_maintenance_open_failed",
     40             Self::Metadata => "state_maintenance_metadata_invalid",
     41             Self::Migration => "state_maintenance_migration_invalid",
     42             Self::Backup => "state_backup_failed",
     43             Self::Restore => "state_restore_failed",
     44             Self::Integrity => "state_integrity_failed",
     45             Self::Recovery => "state_recovery_failed",
     46         }
     47     }
     48 }
     49 
     50 /// Redacted RHI state-maintenance failure.
     51 #[derive(Clone, Copy, PartialEq, Eq)]
     52 pub struct RhiStateMaintenanceError {
     53     kind: RhiStateMaintenanceErrorKind,
     54 }
     55 
     56 impl RhiStateMaintenanceError {
     57     pub(crate) const fn new(kind: RhiStateMaintenanceErrorKind) -> Self {
     58         Self { kind }
     59     }
     60 
     61     pub(crate) fn from_sqlite(error: ServiceSqliteError) -> Self {
     62         let kind = match error.kind() {
     63             ServiceSqliteErrorKind::Authority => RhiStateMaintenanceErrorKind::Authority,
     64             ServiceSqliteErrorKind::Open
     65             | ServiceSqliteErrorKind::Create
     66             | ServiceSqliteErrorKind::Pragma => RhiStateMaintenanceErrorKind::Open,
     67             ServiceSqliteErrorKind::Metadata => RhiStateMaintenanceErrorKind::Metadata,
     68             ServiceSqliteErrorKind::Migration => RhiStateMaintenanceErrorKind::Migration,
     69             ServiceSqliteErrorKind::Backup => RhiStateMaintenanceErrorKind::Backup,
     70             ServiceSqliteErrorKind::Restore => RhiStateMaintenanceErrorKind::Restore,
     71             ServiceSqliteErrorKind::Integrity => RhiStateMaintenanceErrorKind::Integrity,
     72             ServiceSqliteErrorKind::Recovery => RhiStateMaintenanceErrorKind::Recovery,
     73         };
     74         Self::new(kind)
     75     }
     76 
     77     /// Returns the stable failure class.
     78     #[must_use]
     79     pub const fn kind(self) -> RhiStateMaintenanceErrorKind {
     80         self.kind
     81     }
     82 
     83     /// Returns the stable machine-readable failure code.
     84     #[must_use]
     85     pub const fn code(self) -> &'static str {
     86         self.kind.code()
     87     }
     88 }
     89 
     90 impl fmt::Display for RhiStateMaintenanceError {
     91     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     92         formatter.write_str(match self.kind {
     93             RhiStateMaintenanceErrorKind::InvalidEvidence => {
     94                 "RHI state maintenance evidence is invalid"
     95             }
     96             RhiStateMaintenanceErrorKind::InvalidMode => {
     97                 "RHI state maintenance is unavailable in this host mode"
     98             }
     99             RhiStateMaintenanceErrorKind::Catalog => "RHI state catalogs are invalid",
    100             RhiStateMaintenanceErrorKind::Authority => {
    101                 "RHI state maintenance authority could not be established"
    102             }
    103             RhiStateMaintenanceErrorKind::Open => "RHI state maintenance could not open state",
    104             RhiStateMaintenanceErrorKind::Metadata => "RHI state metadata is invalid",
    105             RhiStateMaintenanceErrorKind::Migration => "RHI state migration history is invalid",
    106             RhiStateMaintenanceErrorKind::Backup => "RHI state backup failed",
    107             RhiStateMaintenanceErrorKind::Restore => "RHI state restore failed",
    108             RhiStateMaintenanceErrorKind::Integrity => "RHI state integrity check failed",
    109             RhiStateMaintenanceErrorKind::Recovery => "RHI state recovery failed",
    110         })
    111     }
    112 }
    113 
    114 impl fmt::Debug for RhiStateMaintenanceError {
    115     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    116         formatter
    117             .debug_struct("RhiStateMaintenanceError")
    118             .field("kind", &self.kind)
    119             .finish()
    120     }
    121 }
    122 
    123 impl Error for RhiStateMaintenanceError {}
    124 
    125 /// Retained exact-inode proof of one verified RHI backup.
    126 ///
    127 /// Construction is sealed to [`verify_rhi_state_backup`]. No raw descriptor or
    128 /// pathname is exposed.
    129 ///
    130 /// ```compile_fail
    131 /// use rhi::RhiVerifiedStateBackup;
    132 /// let _ = RhiVerifiedStateBackup { inner: todo!() };
    133 /// ```
    134 pub struct RhiVerifiedStateBackup {
    135     inner: VerifiedServiceBackup,
    136 }
    137 
    138 impl RhiVerifiedStateBackup {
    139     /// Returns the admitted canonical manifest.
    140     #[must_use]
    141     pub const fn manifest(&self) -> &ServiceBackupManifest {
    142         self.inner.manifest()
    143     }
    144 
    145     /// Returns the actual immutable database metadata read from the retained member.
    146     #[must_use]
    147     pub const fn database_metadata(&self) -> &ServiceDatabaseMetadata {
    148         self.inner.database_metadata()
    149     }
    150 }
    151 
    152 impl fmt::Debug for RhiVerifiedStateBackup {
    153     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    154         formatter.write_str("RhiVerifiedStateBackup([redacted])")
    155     }
    156 }
    157 
    158 /// Offline staged RHI replacement that retains exclusive writer authority.
    159 ///
    160 /// Construction is sealed to [`stage_rhi_state_restore`]. Dropping this value
    161 /// preserves the shared exact-inode cleanup and fail-closed evidence contract.
    162 ///
    163 /// ```compile_fail
    164 /// use rhi::RhiStagedStateRestore;
    165 /// let _ = RhiStagedStateRestore { inner: todo!() };
    166 /// ```
    167 pub struct RhiStagedStateRestore {
    168     inner: StagedServiceRestore,
    169 }
    170 
    171 impl fmt::Debug for RhiStagedStateRestore {
    172     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    173         formatter.write_str("RhiStagedStateRestore([redacted])")
    174     }
    175 }
    176 
    177 /// Verifies an untrusted backup bundle against one sealed RHI state identity.
    178 pub fn verify_rhi_state_backup(
    179     manifest_bytes: &[u8],
    180     expected_manifest_digest: BackupManifestSha256,
    181     bundle_directory: &Path,
    182     expected: &RhiStateMetadata,
    183     maximum_state_bytes: NonZeroU64,
    184 ) -> Result<RhiVerifiedStateBackup, RhiStateMaintenanceError> {
    185     verify_backup_bundle(
    186         manifest_bytes,
    187         expected_manifest_digest,
    188         bundle_directory,
    189         &expected.database_identity(),
    190         maximum_state_bytes,
    191     )
    192     .map(|inner| RhiVerifiedStateBackup { inner })
    193     .map_err(RhiStateMaintenanceError::from_sqlite)
    194 }
    195 
    196 /// Copies and completely reverifies a verified backup beside closed RHI state.
    197 ///
    198 /// This operation acquires exclusive writer authority. It never creates a
    199 /// recovery marker or replaces the live database.
    200 pub async fn stage_rhi_state_restore(
    201     runtime: &RhiRuntimeContext,
    202     expected: &RhiStateMetadata,
    203     verified: RhiVerifiedStateBackup,
    204 ) -> Result<RhiStagedStateRestore, RhiStateMaintenanceError> {
    205     state_host::require_metadata(runtime, expected).map_err(|_| {
    206         RhiStateMaintenanceError::new(RhiStateMaintenanceErrorKind::InvalidEvidence)
    207     })?;
    208     let paths = state_host::state_paths(runtime).map_err(|_| {
    209         RhiStateMaintenanceError::new(RhiStateMaintenanceErrorKind::InvalidEvidence)
    210     })?;
    211     let (migrations, schema) = state_host::catalogs()
    212         .map_err(|_| RhiStateMaintenanceError::new(RhiStateMaintenanceErrorKind::Catalog))?;
    213     stage_verified_restore(
    214         &paths,
    215         &expected.database_identity(),
    216         &migrations,
    217         &schema,
    218         verified.inner,
    219     )
    220     .await
    221     .map(|inner| RhiStagedStateRestore { inner })
    222     .map_err(RhiStateMaintenanceError::from_sqlite)
    223 }
    224 
    225 /// Atomically installs a completely verified staged RHI restore.
    226 ///
    227 /// Success intentionally returns no open host. The next writable open owns
    228 /// exact recovery-evidence reconciliation before SQLite is exposed again.
    229 pub async fn finalize_rhi_state_restore(
    230     staged: RhiStagedStateRestore,
    231 ) -> Result<(), RhiStateMaintenanceError> {
    232     finalize_staged_restore(staged.inner)
    233         .await
    234         .map_err(RhiStateMaintenanceError::from_sqlite)
    235 }
    236 
    237 #[cfg(test)]
    238 mod tests {
    239     use super::*;
    240 
    241     #[test]
    242     fn shared_failures_map_to_the_closed_source_free_rhi_vocabulary() {
    243         for (source, expected) in [
    244             (
    245                 ServiceSqliteErrorKind::Authority,
    246                 RhiStateMaintenanceErrorKind::Authority,
    247             ),
    248             (
    249                 ServiceSqliteErrorKind::Open,
    250                 RhiStateMaintenanceErrorKind::Open,
    251             ),
    252             (
    253                 ServiceSqliteErrorKind::Create,
    254                 RhiStateMaintenanceErrorKind::Open,
    255             ),
    256             (
    257                 ServiceSqliteErrorKind::Pragma,
    258                 RhiStateMaintenanceErrorKind::Open,
    259             ),
    260             (
    261                 ServiceSqliteErrorKind::Metadata,
    262                 RhiStateMaintenanceErrorKind::Metadata,
    263             ),
    264             (
    265                 ServiceSqliteErrorKind::Migration,
    266                 RhiStateMaintenanceErrorKind::Migration,
    267             ),
    268             (
    269                 ServiceSqliteErrorKind::Backup,
    270                 RhiStateMaintenanceErrorKind::Backup,
    271             ),
    272             (
    273                 ServiceSqliteErrorKind::Restore,
    274                 RhiStateMaintenanceErrorKind::Restore,
    275             ),
    276             (
    277                 ServiceSqliteErrorKind::Integrity,
    278                 RhiStateMaintenanceErrorKind::Integrity,
    279             ),
    280             (
    281                 ServiceSqliteErrorKind::Recovery,
    282                 RhiStateMaintenanceErrorKind::Recovery,
    283             ),
    284         ] {
    285             let mapped = RhiStateMaintenanceError::from_sqlite(ServiceSqliteError::with_source(
    286                 source,
    287                 SensitiveSource,
    288             ));
    289             assert_eq!(mapped.kind(), expected);
    290             assert!(Error::source(&mapped).is_none());
    291             let rendered = format!("{mapped} {mapped:?}");
    292             assert!(!rendered.contains("sensitive"));
    293             assert!(!mapped.code().is_empty());
    294         }
    295     }
    296 
    297     #[derive(Debug)]
    298     struct SensitiveSource;
    299 
    300     impl fmt::Display for SensitiveSource {
    301         fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    302             formatter.write_str("sensitive /tmp/state.sqlite")
    303         }
    304     }
    305 
    306     impl Error for SensitiveSource {}
    307 }