rhi

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

state_metadata.rs (18540B)


      1 //! Immutable RHI-specific identity and policy evidence for one state host.
      2 
      3 use core::fmt;
      4 use std::{collections::BTreeMap, error::Error};
      5 
      6 use nostr::PublicKey;
      7 use radroots_service_sqlite::{
      8     ServiceDatabaseIdentity, ServiceDatabaseMetadata, ServiceSqliteApplicationId,
      9     ServiceSqlitePaths,
     10 };
     11 use radroots_storage::event::SourceGeneration;
     12 use serde_json::{Value, json};
     13 use sha2::{Digest, Sha256};
     14 
     15 use crate::{
     16     RHI_CONFIG_SCHEMA_VERSION, RHI_STATE_BASE_SCHEMA_VERSION, RHI_STATE_SCHEMA_VERSION,
     17     RhiBootstrapProfileV1, RhiConfigDocumentV1, RhiConfigProfile, RhiRuntimeContext,
     18 };
     19 
     20 const NORMALIZED_CONFIG_DIGEST_DOMAIN: &[u8] = b"radroots.rhi.normalized_config.v1\0";
     21 const EVIDENCE_POLICY_DIGEST_DOMAIN: &[u8] = b"radroots:rhi-evidence-policy:v1\0";
     22 
     23 /// SQLite application identity for RHI, encoded as ASCII `RDRH`.
     24 pub const RHI_STATE_APPLICATION_ID: u32 = 0x5244_5248;
     25 
     26 /// Exact version of the governed RHI admin/operator contract.
     27 pub const RHI_ADMIN_CONTRACT_VERSION: u32 = 1;
     28 
     29 /// Exact version of the governed RHI status contract.
     30 pub const RHI_STATUS_CONTRACT_VERSION: u32 = 1;
     31 
     32 /// Exact version of the governed RHI identity-provider contract.
     33 pub const RHI_PROVIDER_CONTRACT_VERSION: u32 = 1;
     34 
     35 /// SHA-256 identity of one fully defaulted normalized RHI configuration.
     36 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
     37 pub struct RhiNormalizedConfigDigest([u8; 32]);
     38 
     39 impl RhiNormalizedConfigDigest {
     40     /// Returns the exact digest bytes.
     41     #[must_use]
     42     pub const fn as_bytes(&self) -> &[u8; 32] {
     43         &self.0
     44     }
     45 }
     46 
     47 impl fmt::Debug for RhiNormalizedConfigDigest {
     48     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     49         formatter.write_str("RhiNormalizedConfigDigest([redacted])")
     50     }
     51 }
     52 
     53 /// SHA-256 identity of the normalized configured evidence policy.
     54 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
     55 pub struct RhiEvidencePolicyDigest([u8; 32]);
     56 
     57 impl RhiEvidencePolicyDigest {
     58     pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self {
     59         Self(bytes)
     60     }
     61 
     62     /// Returns the exact digest bytes.
     63     #[must_use]
     64     pub const fn as_bytes(&self) -> &[u8; 32] {
     65         &self.0
     66     }
     67 }
     68 
     69 impl fmt::Debug for RhiEvidencePolicyDigest {
     70     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     71         formatter.write_str("RhiEvidencePolicyDigest([redacted])")
     72     }
     73 }
     74 
     75 /// One validated canonical expected RHI service public identity.
     76 #[derive(Clone, PartialEq, Eq, Hash)]
     77 pub struct RhiExpectedPublicIdentity(Box<str>);
     78 
     79 impl RhiExpectedPublicIdentity {
     80     /// Returns the canonical lowercase 32-byte x-only public key in hex.
     81     #[must_use]
     82     pub fn as_hex(&self) -> &str {
     83         &self.0
     84     }
     85 
     86     fn from_hex(value: &str) -> Result<Self, RhiStateMetadataError> {
     87         let public_key = PublicKey::from_hex(value)
     88             .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Identity))?;
     89         public_key
     90             .xonly()
     91             .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Identity))?;
     92         let canonical = public_key.to_hex();
     93         if canonical != value {
     94             return Err(RhiStateMetadataError::new(
     95                 RhiStateMetadataErrorKind::Identity,
     96             ));
     97         }
     98         Ok(Self(canonical.into_boxed_str()))
     99     }
    100 }
    101 
    102 impl fmt::Debug for RhiExpectedPublicIdentity {
    103     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    104         formatter.write_str("RhiExpectedPublicIdentity([redacted])")
    105     }
    106 }
    107 
    108 /// Exact shared contract versions bound to one RHI state-host session.
    109 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    110 pub struct RhiStatePolicyVersions {
    111     configuration: u32,
    112     state: u32,
    113     admin: u32,
    114     status: u32,
    115     provider: u32,
    116 }
    117 
    118 impl RhiStatePolicyVersions {
    119     const fn governed() -> Self {
    120         Self {
    121             configuration: RHI_CONFIG_SCHEMA_VERSION,
    122             state: RHI_STATE_SCHEMA_VERSION,
    123             admin: RHI_ADMIN_CONTRACT_VERSION,
    124             status: RHI_STATUS_CONTRACT_VERSION,
    125             provider: RHI_PROVIDER_CONTRACT_VERSION,
    126         }
    127     }
    128 
    129     /// Returns the exact configuration-contract version.
    130     #[must_use]
    131     pub const fn configuration(self) -> u32 {
    132         self.configuration
    133     }
    134 
    135     /// Returns the exact state-schema version.
    136     #[must_use]
    137     pub const fn state(self) -> u32 {
    138         self.state
    139     }
    140 
    141     /// Returns the exact admin/operator-contract version.
    142     #[must_use]
    143     pub const fn admin(self) -> u32 {
    144         self.admin
    145     }
    146 
    147     /// Returns the exact status-contract version.
    148     #[must_use]
    149     pub const fn status(self) -> u32 {
    150         self.status
    151     }
    152 
    153     /// Returns the exact identity-provider-contract version.
    154     #[must_use]
    155     pub const fn provider(self) -> u32 {
    156         self.provider
    157     }
    158 }
    159 
    160 /// Non-forgeable RHI metadata bound to one runtime context and configuration.
    161 #[derive(Clone, PartialEq, Eq)]
    162 pub struct RhiStateMetadata {
    163     paths: ServiceSqlitePaths,
    164     database: ServiceDatabaseMetadata,
    165     database_identity: ServiceDatabaseIdentity,
    166     configuration: RhiNormalizedConfigDigest,
    167     evidence_policy: RhiEvidencePolicyDigest,
    168     identity: RhiExpectedPublicIdentity,
    169     policy_versions: RhiStatePolicyVersions,
    170 }
    171 
    172 impl RhiStateMetadata {
    173     /// Derives all state evidence from one sealed runtime context, one admitted
    174     /// normalized configuration, and caller-injected generation/time evidence.
    175     pub fn new(
    176         runtime: &RhiRuntimeContext,
    177         configuration: &RhiConfigDocumentV1,
    178         source_generation: SourceGeneration,
    179         created_at_unix_ms: u64,
    180     ) -> Result<Self, RhiStateMetadataError> {
    181         require_profile_binding(runtime.profile(), configuration.profile())?;
    182         let paths = ServiceSqlitePaths::from_runtime_context(runtime.context())
    183             .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Paths))?;
    184         let application_id = ServiceSqliteApplicationId::new(RHI_STATE_APPLICATION_ID)
    185             .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Invariant))?;
    186         let state_schema_version = core::num::NonZeroU32::new(RHI_STATE_BASE_SCHEMA_VERSION)
    187             .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Invariant))?;
    188         let database = ServiceDatabaseMetadata::new(
    189             &paths,
    190             source_generation,
    191             state_schema_version,
    192             created_at_unix_ms,
    193             application_id,
    194         )
    195         .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Database))?;
    196         let supported_state_schema_version =
    197             core::num::NonZeroU32::new(RHI_STATE_SCHEMA_VERSION)
    198                 .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Invariant))?;
    199         let database_identity = ServiceDatabaseIdentity::new(
    200             &paths,
    201             source_generation,
    202             supported_state_schema_version,
    203             application_id,
    204         );
    205         let normalized = configuration.normalized();
    206         let configuration_digest = normalized_config_digest(configuration.profile(), normalized)?;
    207         let evidence_policy = evidence_policy_digest(normalized)?;
    208         let identity = expected_identity(normalized)?;
    209         let policy_versions = RhiStatePolicyVersions::governed();
    210         if [
    211             policy_versions.configuration,
    212             policy_versions.state,
    213             policy_versions.admin,
    214             policy_versions.status,
    215             policy_versions.provider,
    216         ]
    217         .contains(&0)
    218         {
    219             return Err(RhiStateMetadataError::new(
    220                 RhiStateMetadataErrorKind::Invariant,
    221             ));
    222         }
    223         Ok(Self {
    224             paths,
    225             database,
    226             database_identity,
    227             configuration: configuration_digest,
    228             evidence_policy,
    229             identity,
    230             policy_versions,
    231         })
    232     }
    233 
    234     pub(crate) fn from_existing_database(
    235         runtime: &RhiRuntimeContext,
    236         configuration: &RhiConfigDocumentV1,
    237         actual: &ServiceDatabaseMetadata,
    238     ) -> Result<Self, RhiStateMetadataError> {
    239         let expected_application = ServiceSqliteApplicationId::new(RHI_STATE_APPLICATION_ID)
    240             .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Invariant))?;
    241         if actual.service() != runtime.context().service()
    242             || actual.instance() != runtime.context().instance()
    243             || actual.application_id() != expected_application
    244             || actual.state_schema_version().get() < RHI_STATE_BASE_SCHEMA_VERSION
    245             || actual.state_schema_version().get() > RHI_STATE_SCHEMA_VERSION
    246         {
    247             return Err(RhiStateMetadataError::new(
    248                 RhiStateMetadataErrorKind::Database,
    249             ));
    250         }
    251         Self::new(
    252             runtime,
    253             configuration,
    254             actual.source_generation(),
    255             actual.created_at_unix_ms(),
    256         )
    257     }
    258 
    259     /// Returns the immutable shared schema-v1 initialization metadata.
    260     #[must_use]
    261     pub const fn initial_database_metadata(&self) -> &ServiceDatabaseMetadata {
    262         &self.database
    263     }
    264 
    265     /// Returns the immutable shared schema-v1 initialization metadata.
    266     #[must_use]
    267     pub const fn database(&self) -> &ServiceDatabaseMetadata {
    268         self.initial_database_metadata()
    269     }
    270 
    271     /// Returns the reopen identity derived from the immutable database metadata.
    272     #[must_use]
    273     pub fn database_identity(&self) -> ServiceDatabaseIdentity {
    274         self.database_identity.clone()
    275     }
    276 
    277     /// Returns the normalized configuration digest.
    278     #[must_use]
    279     pub const fn configuration_digest(&self) -> RhiNormalizedConfigDigest {
    280         self.configuration
    281     }
    282 
    283     /// Returns the normalized evidence-policy digest.
    284     #[must_use]
    285     pub const fn evidence_policy_digest(&self) -> RhiEvidencePolicyDigest {
    286         self.evidence_policy
    287     }
    288 
    289     /// Returns the exact configured service identity binding.
    290     #[must_use]
    291     pub const fn expected_identity(&self) -> &RhiExpectedPublicIdentity {
    292         &self.identity
    293     }
    294 
    295     /// Returns the exact governed policy versions.
    296     #[must_use]
    297     pub const fn policy_versions(&self) -> RhiStatePolicyVersions {
    298         self.policy_versions
    299     }
    300 
    301     pub(crate) fn matches_runtime(&self, runtime: &RhiRuntimeContext) -> bool {
    302         ServiceSqlitePaths::from_runtime_context(runtime.context())
    303             .is_ok_and(|paths| paths == self.paths)
    304     }
    305 
    306     pub(crate) const fn paths(&self) -> &ServiceSqlitePaths {
    307         &self.paths
    308     }
    309 
    310     pub(crate) fn matches_configuration(&self, configuration: &RhiConfigDocumentV1) -> bool {
    311         normalized_config_digest(configuration.profile(), configuration.normalized())
    312             .is_ok_and(|digest| digest == self.configuration)
    313             && evidence_policy_digest(configuration.normalized())
    314                 .is_ok_and(|digest| digest == self.evidence_policy)
    315             && expected_identity(configuration.normalized())
    316                 .is_ok_and(|identity| identity == self.identity)
    317     }
    318 }
    319 
    320 impl fmt::Debug for RhiStateMetadata {
    321     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    322         formatter
    323             .debug_struct("RhiStateMetadata")
    324             .field("database", &self.database)
    325             .field("configuration", &self.configuration)
    326             .field("evidence_policy", &self.evidence_policy)
    327             .field("identity", &"[redacted]")
    328             .field("policy_versions", &self.policy_versions)
    329             .field("paths", &"[redacted]")
    330             .finish()
    331     }
    332 }
    333 
    334 /// Stable source-free class for invalid RHI state metadata.
    335 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    336 pub enum RhiStateMetadataErrorKind {
    337     Profile,
    338     Paths,
    339     Configuration,
    340     EvidencePolicy,
    341     Identity,
    342     Database,
    343     Invariant,
    344 }
    345 
    346 /// Source-free RHI state-metadata construction failure.
    347 #[derive(Clone, Copy, PartialEq, Eq)]
    348 pub struct RhiStateMetadataError {
    349     kind: RhiStateMetadataErrorKind,
    350 }
    351 
    352 impl RhiStateMetadataError {
    353     const fn new(kind: RhiStateMetadataErrorKind) -> Self {
    354         Self { kind }
    355     }
    356 
    357     /// Returns the stable failure classification.
    358     #[must_use]
    359     pub const fn kind(self) -> RhiStateMetadataErrorKind {
    360         self.kind
    361     }
    362 }
    363 
    364 impl fmt::Display for RhiStateMetadataError {
    365     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    366         formatter.write_str(match self.kind {
    367             RhiStateMetadataErrorKind::Profile => "RHI configuration profile is inconsistent",
    368             RhiStateMetadataErrorKind::Paths => "RHI state metadata paths are invalid",
    369             RhiStateMetadataErrorKind::Configuration => {
    370                 "RHI normalized configuration identity is invalid"
    371             }
    372             RhiStateMetadataErrorKind::EvidencePolicy => {
    373                 "RHI normalized evidence-policy identity is invalid"
    374             }
    375             RhiStateMetadataErrorKind::Identity => "RHI expected identity binding is invalid",
    376             RhiStateMetadataErrorKind::Database => "RHI database metadata is invalid",
    377             RhiStateMetadataErrorKind::Invariant => "RHI metadata contract is invalid",
    378         })
    379     }
    380 }
    381 
    382 impl fmt::Debug for RhiStateMetadataError {
    383     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    384         formatter
    385             .debug_struct("RhiStateMetadataError")
    386             .field("kind", &self.kind)
    387             .finish()
    388     }
    389 }
    390 
    391 impl Error for RhiStateMetadataError {}
    392 
    393 fn require_profile_binding(
    394     runtime: RhiBootstrapProfileV1,
    395     configuration: RhiConfigProfile,
    396 ) -> Result<(), RhiStateMetadataError> {
    397     let matches = match runtime {
    398         RhiBootstrapProfileV1::ServiceHost | RhiBootstrapProfileV1::Interactive => {
    399             configuration == RhiConfigProfile::Production
    400         }
    401         RhiBootstrapProfileV1::RepoLocal => configuration == RhiConfigProfile::RepoLocal,
    402     };
    403     matches
    404         .then_some(())
    405         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Profile))
    406 }
    407 
    408 pub(crate) fn normalized_config_digest(
    409     profile: RhiConfigProfile,
    410     normalized: &Value,
    411 ) -> Result<RhiNormalizedConfigDigest, RhiStateMetadataError> {
    412     let bytes = serde_json::to_vec(normalized)
    413         .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Configuration))?;
    414     let length = u64::try_from(bytes.len())
    415         .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Configuration))?;
    416     let mut hasher = Sha256::new();
    417     hasher.update(NORMALIZED_CONFIG_DIGEST_DOMAIN);
    418     hasher.update([match profile {
    419         RhiConfigProfile::Production => 0,
    420         RhiConfigProfile::RepoLocal => 1,
    421     }]);
    422     hasher.update(length.to_be_bytes());
    423     hasher.update(bytes);
    424     Ok(RhiNormalizedConfigDigest(hasher.finalize().into()))
    425 }
    426 
    427 pub(crate) fn evidence_policy_digest(
    428     normalized: &Value,
    429 ) -> Result<RhiEvidencePolicyDigest, RhiStateMetadataError> {
    430     let relays = normalized
    431         .pointer("/relays")
    432         .and_then(Value::as_array)
    433         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))?;
    434     let mut relay_urls = BTreeMap::new();
    435     for relay in relays {
    436         let id = string(relay, "/id")?;
    437         let url = string(relay, "/url")?;
    438         if relay_urls.insert(id, url).is_some() {
    439             return Err(RhiStateMetadataError::new(
    440                 RhiStateMetadataErrorKind::EvidencePolicy,
    441             ));
    442         }
    443     }
    444 
    445     let evidence = normalized
    446         .pointer("/evidence")
    447         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))?;
    448     let sources = evidence
    449         .pointer("/sources")
    450         .and_then(Value::as_array)
    451         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))?;
    452     let mut normalized_sources = Vec::with_capacity(sources.len());
    453     for source in sources {
    454         let relay_id = string(source, "/relay_id")?;
    455         let relay_url = relay_urls
    456             .get(relay_id)
    457             .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))?;
    458         normalized_sources.push(json!({
    459             "completion": "nostr_eose_before_deadline",
    460             "deadline_ms": integer(source, "/deadline_ms")?,
    461             "kind": string(source, "/kind")?,
    462             "lookback_seconds": integer(source, "/lookback_seconds")?,
    463             "overlap_seconds": integer(source, "/overlap_seconds")?,
    464             "relay_id": relay_id,
    465             "relay_url": relay_url,
    466             "required": boolean(source, "/required")?,
    467             "selector": string(source, "/selector")?,
    468             "source_id": string(source, "/source_id")?,
    469         }));
    470     }
    471     normalized_sources
    472         .sort_by(|left, right| left["source_id"].as_str().cmp(&right["source_id"].as_str()));
    473     let policy = json!({
    474         "contract": string(evidence, "/contract")?,
    475         "contract_version": integer(evidence, "/contract_version")?,
    476         "policy_id": string(evidence, "/policy_id")?,
    477         "sources": normalized_sources,
    478     });
    479     let bytes = serde_json::to_vec(&policy)
    480         .map_err(|_| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))?;
    481     let mut hasher = Sha256::new();
    482     hasher.update(EVIDENCE_POLICY_DIGEST_DOMAIN);
    483     hasher.update(bytes);
    484     Ok(RhiEvidencePolicyDigest(hasher.finalize().into()))
    485 }
    486 
    487 fn expected_identity(
    488     normalized: &Value,
    489 ) -> Result<RhiExpectedPublicIdentity, RhiStateMetadataError> {
    490     normalized
    491         .pointer("/identity/service/expected_public_key")
    492         .and_then(Value::as_str)
    493         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::Identity))
    494         .and_then(RhiExpectedPublicIdentity::from_hex)
    495 }
    496 
    497 fn string<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, RhiStateMetadataError> {
    498     value
    499         .pointer(pointer)
    500         .and_then(Value::as_str)
    501         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))
    502 }
    503 
    504 fn integer(value: &Value, pointer: &str) -> Result<u64, RhiStateMetadataError> {
    505     value
    506         .pointer(pointer)
    507         .and_then(Value::as_u64)
    508         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))
    509 }
    510 
    511 fn boolean(value: &Value, pointer: &str) -> Result<bool, RhiStateMetadataError> {
    512     value
    513         .pointer(pointer)
    514         .and_then(Value::as_bool)
    515         .ok_or_else(|| RhiStateMetadataError::new(RhiStateMetadataErrorKind::EvidencePolicy))
    516 }