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 }