myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

state_metadata.rs (22845B)


      1 //! Immutable Myc-specific identity and policy evidence for one state host.
      2 
      3 use core::fmt;
      4 use std::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 sha2::{Digest, Sha256};
     13 
     14 use crate::nip46_authorization::MycConnectionPolicies;
     15 use crate::state_connection::{
     16     MycAuthorizationChallengeRecord, MycAuthorizationChallengeRequest,
     17     MycConnectionAdmissionRequest, MycConnectionOperatorDecision, MycConnectionTimeUnixMs,
     18 };
     19 use crate::state_delivery::{MycDeliveryPolicies, MycDeliveryPolicyMode, MycDeliveryRelayId};
     20 use crate::state_discovery::MycDiscoveryPolicies;
     21 use crate::state_governance::{
     22     MycGovernancePolicies, MycRateLimitClass, MycRateLimitPolicy, MycRateRelayId,
     23 };
     24 use crate::{
     25     MYC_CONFIG_SCHEMA_VERSION, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION,
     26     MycBootstrapProfileV1, MycConfigDocumentV1, MycConfigProfile, MycRuntimeContext,
     27 };
     28 
     29 const NORMALIZED_CONFIG_DIGEST_DOMAIN: &[u8] = b"radroots.myc.normalized_config.v1\0";
     30 
     31 /// SQLite application identity for Myc, encoded as ASCII `RDMY`.
     32 pub const MYC_STATE_APPLICATION_ID: u32 = 0x5244_4d59;
     33 
     34 /// Exact version of the governed Myc operator contract.
     35 pub const MYC_OPERATOR_CONTRACT_VERSION: u32 = 1;
     36 /// Exact version of the governed Myc status contract.
     37 pub const MYC_SIGNER_STATUS_CONTRACT_VERSION: u32 = 1;
     38 
     39 /// SHA-256 identity of one fully defaulted normalized Myc configuration.
     40 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
     41 pub struct MycNormalizedConfigDigest([u8; 32]);
     42 
     43 impl MycNormalizedConfigDigest {
     44     /// Returns the exact digest bytes.
     45     #[must_use]
     46     pub const fn as_bytes(&self) -> &[u8; 32] {
     47         &self.0
     48     }
     49 }
     50 
     51 impl fmt::Debug for MycNormalizedConfigDigest {
     52     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     53         formatter.write_str("MycNormalizedConfigDigest([redacted])")
     54     }
     55 }
     56 
     57 /// One validated canonical expected Nostr public identity.
     58 #[derive(Clone, PartialEq, Eq, Hash)]
     59 pub struct MycExpectedPublicIdentity(Box<str>);
     60 
     61 impl MycExpectedPublicIdentity {
     62     /// Returns the canonical lowercase 32-byte x-only public key in hex.
     63     #[must_use]
     64     pub fn as_hex(&self) -> &str {
     65         &self.0
     66     }
     67 
     68     fn from_hex(value: &str) -> Result<Self, MycStateMetadataError> {
     69         let public_key = PublicKey::from_hex(value)
     70             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?;
     71         public_key
     72             .xonly()
     73             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?;
     74         let canonical = public_key.to_hex();
     75         if canonical != value {
     76             return Err(MycStateMetadataError::new(
     77                 MycStateMetadataErrorKind::Identity,
     78             ));
     79         }
     80         Ok(Self(canonical.into_boxed_str()))
     81     }
     82 }
     83 
     84 impl fmt::Debug for MycExpectedPublicIdentity {
     85     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     86         formatter.write_str("MycExpectedPublicIdentity([redacted])")
     87     }
     88 }
     89 
     90 /// Exact expected identities for every enabled Myc role.
     91 #[derive(Clone, PartialEq, Eq)]
     92 pub struct MycExpectedIdentities {
     93     transport: MycExpectedPublicIdentity,
     94     user: MycExpectedPublicIdentity,
     95     discovery: Option<MycExpectedPublicIdentity>,
     96 }
     97 
     98 impl MycExpectedIdentities {
     99     /// Returns the required transport identity.
    100     #[must_use]
    101     pub const fn transport(&self) -> &MycExpectedPublicIdentity {
    102         &self.transport
    103     }
    104 
    105     /// Returns the required user identity.
    106     #[must_use]
    107     pub const fn user(&self) -> &MycExpectedPublicIdentity {
    108         &self.user
    109     }
    110 
    111     /// Returns the discovery identity only when discovery is enabled.
    112     #[must_use]
    113     pub const fn discovery(&self) -> Option<&MycExpectedPublicIdentity> {
    114         self.discovery.as_ref()
    115     }
    116 }
    117 
    118 impl fmt::Debug for MycExpectedIdentities {
    119     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    120         formatter
    121             .debug_struct("MycExpectedIdentities")
    122             .field("transport", &"[redacted]")
    123             .field("user", &"[redacted]")
    124             .field("discovery", &self.discovery.as_ref().map(|_| "[redacted]"))
    125             .finish()
    126     }
    127 }
    128 
    129 /// Exact contract versions bound to one Myc state-host session.
    130 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    131 pub struct MycStatePolicyVersions {
    132     configuration: u32,
    133     state: u32,
    134     operator: u32,
    135     status: u32,
    136 }
    137 
    138 impl MycStatePolicyVersions {
    139     const fn governed() -> Self {
    140         Self {
    141             configuration: MYC_CONFIG_SCHEMA_VERSION,
    142             state: MYC_STATE_SCHEMA_VERSION,
    143             operator: MYC_OPERATOR_CONTRACT_VERSION,
    144             status: MYC_SIGNER_STATUS_CONTRACT_VERSION,
    145         }
    146     }
    147 
    148     #[must_use]
    149     /// Returns the exact configuration-contract version.
    150     pub const fn configuration(self) -> u32 {
    151         self.configuration
    152     }
    153 
    154     #[must_use]
    155     /// Returns the exact state-schema version.
    156     pub const fn state(self) -> u32 {
    157         self.state
    158     }
    159 
    160     #[must_use]
    161     /// Returns the exact operator-contract version.
    162     pub const fn operator(self) -> u32 {
    163         self.operator
    164     }
    165 
    166     #[must_use]
    167     /// Returns the exact signer-status contract version.
    168     pub const fn status(self) -> u32 {
    169         self.status
    170     }
    171 }
    172 
    173 /// Non-forgeable Myc metadata bound to one runtime context and configuration.
    174 #[derive(Clone, PartialEq, Eq)]
    175 pub struct MycStateMetadata {
    176     paths: ServiceSqlitePaths,
    177     database: ServiceDatabaseMetadata,
    178     database_identity: ServiceDatabaseIdentity,
    179     configuration: MycNormalizedConfigDigest,
    180     identities: MycExpectedIdentities,
    181     governance: MycGovernancePolicies,
    182     authorization: MycConnectionPolicies,
    183     delivery: MycDeliveryPolicies,
    184     discovery: Option<MycDiscoveryPolicies>,
    185     policy_versions: MycStatePolicyVersions,
    186 }
    187 
    188 impl MycStateMetadata {
    189     /// Derives all state evidence from one sealed runtime context, one admitted
    190     /// normalized configuration, and caller-injected generation/time evidence.
    191     pub fn new(
    192         runtime: &MycRuntimeContext,
    193         configuration: &MycConfigDocumentV1,
    194         source_generation: SourceGeneration,
    195         created_at_unix_ms: u64,
    196     ) -> Result<Self, MycStateMetadataError> {
    197         require_profile_binding(runtime.profile(), configuration.profile())?;
    198         let paths = ServiceSqlitePaths::from_runtime_context(runtime.context())
    199             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Paths))?;
    200         let application_id = ServiceSqliteApplicationId::new(MYC_STATE_APPLICATION_ID)
    201             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    202         let state_schema_version = core::num::NonZeroU32::new(MYC_STATE_BASE_SCHEMA_VERSION)
    203             .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    204         let database = ServiceDatabaseMetadata::new(
    205             &paths,
    206             source_generation,
    207             state_schema_version,
    208             created_at_unix_ms,
    209             application_id,
    210         )
    211         .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Database))?;
    212         let supported_state_schema_version =
    213             core::num::NonZeroU32::new(MYC_STATE_SCHEMA_VERSION)
    214                 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    215         let database_identity = ServiceDatabaseIdentity::new(
    216             &paths,
    217             source_generation,
    218             supported_state_schema_version,
    219             application_id,
    220         );
    221         let normalized = configuration.normalized();
    222         let governance = governance_policies(normalized)?;
    223         let authorization = MycConnectionPolicies::from_normalized(normalized)
    224             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    225         if !authorization.retention_is_bounded() {
    226             return Err(MycStateMetadataError::new(
    227                 MycStateMetadataErrorKind::Invariant,
    228             ));
    229         }
    230         let identities = expected_identities(normalized)?;
    231         let delivery = delivery_policies(normalized)?;
    232         let discovery = MycDiscoveryPolicies::from_normalized(normalized, &identities)
    233             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    234         let configuration = normalized_config_digest(configuration.profile(), normalized)?;
    235         let policy_versions = MycStatePolicyVersions::governed();
    236         if [
    237             policy_versions.configuration,
    238             policy_versions.state,
    239             policy_versions.operator,
    240             policy_versions.status,
    241         ]
    242         .contains(&0)
    243         {
    244             return Err(MycStateMetadataError::new(
    245                 MycStateMetadataErrorKind::Invariant,
    246             ));
    247         }
    248         Ok(Self {
    249             paths,
    250             database,
    251             database_identity,
    252             configuration,
    253             identities,
    254             governance,
    255             authorization,
    256             delivery,
    257             discovery,
    258             policy_versions,
    259         })
    260     }
    261 
    262     pub(crate) fn from_existing_database(
    263         runtime: &MycRuntimeContext,
    264         configuration: &MycConfigDocumentV1,
    265         actual: &ServiceDatabaseMetadata,
    266     ) -> Result<Self, MycStateMetadataError> {
    267         let expected_application = ServiceSqliteApplicationId::new(MYC_STATE_APPLICATION_ID)
    268             .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?;
    269         if actual.service() != runtime.context().service()
    270             || actual.instance() != runtime.context().instance()
    271             || actual.application_id() != expected_application
    272             || actual.state_schema_version().get() < MYC_STATE_BASE_SCHEMA_VERSION
    273             || actual.state_schema_version().get() > MYC_STATE_SCHEMA_VERSION
    274         {
    275             return Err(MycStateMetadataError::new(
    276                 MycStateMetadataErrorKind::Database,
    277             ));
    278         }
    279         Self::new(
    280             runtime,
    281             configuration,
    282             actual.source_generation(),
    283             actual.created_at_unix_ms(),
    284         )
    285     }
    286 
    287     /// Returns the immutable shared schema-v1 initialization metadata.
    288     #[must_use]
    289     pub const fn initial_database_metadata(&self) -> &ServiceDatabaseMetadata {
    290         &self.database
    291     }
    292 
    293     /// Returns the reopen identity with this binary's exact schema ceiling.
    294     #[must_use]
    295     pub fn database_identity(&self) -> ServiceDatabaseIdentity {
    296         self.database_identity.clone()
    297     }
    298 
    299     /// Returns the normalized configuration digest.
    300     #[must_use]
    301     pub const fn configuration_digest(&self) -> MycNormalizedConfigDigest {
    302         self.configuration
    303     }
    304 
    305     /// Returns the exact configured identity-role bindings.
    306     #[must_use]
    307     pub const fn expected_identities(&self) -> &MycExpectedIdentities {
    308         &self.identities
    309     }
    310 
    311     /// Returns the exact governed policy versions.
    312     #[must_use]
    313     pub const fn policy_versions(&self) -> MycStatePolicyVersions {
    314         self.policy_versions
    315     }
    316 
    317     pub(crate) const fn governance_rate_policy(
    318         &self,
    319         class: MycRateLimitClass,
    320     ) -> MycRateLimitPolicy {
    321         self.governance.rate_policy(class)
    322     }
    323 
    324     pub(crate) fn admits_rate_relay(&self, relay: &MycRateRelayId) -> bool {
    325         self.governance.admits_relay(relay)
    326     }
    327 
    328     pub(crate) const fn governance_audit_retention_ms(&self) -> u64 {
    329         self.governance.audit_retention_ms()
    330     }
    331 
    332     pub(crate) fn admits_connection_request(
    333         &self,
    334         request: &MycConnectionAdmissionRequest,
    335     ) -> bool {
    336         self.authorization.admits_connection_request(request)
    337     }
    338 
    339     pub(crate) fn admits_connection_operator_decision(
    340         &self,
    341         observed_at: MycConnectionTimeUnixMs,
    342         decision: &MycConnectionOperatorDecision,
    343     ) -> bool {
    344         self.authorization
    345             .admits_operator_decision(observed_at, decision)
    346     }
    347 
    348     pub(crate) fn admits_authorization_challenge_request(
    349         &self,
    350         request: &MycAuthorizationChallengeRequest,
    351     ) -> bool {
    352         self.authorization.admits_challenge_request(request)
    353     }
    354 
    355     pub(crate) fn authorization_challenge_is_current(
    356         &self,
    357         record: &MycAuthorizationChallengeRecord,
    358         observed_at: MycConnectionTimeUnixMs,
    359     ) -> bool {
    360         self.authorization.challenge_is_current(record, observed_at)
    361     }
    362 
    363     pub(crate) const fn delivery_policies(&self) -> &MycDeliveryPolicies {
    364         &self.delivery
    365     }
    366 
    367     pub(crate) const fn outbox_maximum(&self) -> usize {
    368         self.delivery.outbox_maximum()
    369     }
    370 
    371     pub(crate) const fn discovery_policies(&self) -> Option<&MycDiscoveryPolicies> {
    372         self.discovery.as_ref()
    373     }
    374 
    375     pub(crate) fn matches_runtime(&self, runtime: &MycRuntimeContext) -> bool {
    376         ServiceSqlitePaths::from_runtime_context(runtime.context())
    377             .is_ok_and(|paths| paths == self.paths)
    378     }
    379 
    380     pub(crate) fn matches_configuration(
    381         &self,
    382         runtime: &MycRuntimeContext,
    383         configuration: &MycConfigDocumentV1,
    384     ) -> bool {
    385         self.matches_runtime(runtime)
    386             && require_profile_binding(runtime.profile(), configuration.profile()).is_ok()
    387             && normalized_config_digest(configuration.profile(), configuration.normalized())
    388                 .is_ok_and(|digest| digest == self.configuration)
    389     }
    390 }
    391 
    392 impl fmt::Debug for MycStateMetadata {
    393     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    394         formatter
    395             .debug_struct("MycStateMetadata")
    396             .field("database", &self.database)
    397             .field("database_identity", &self.database_identity)
    398             .field("configuration", &self.configuration)
    399             .field("identities", &self.identities)
    400             .field("governance", &"[redacted]")
    401             .field("delivery", &"[redacted]")
    402             .field("outbox_maximum", &self.delivery.outbox_maximum())
    403             .field("discovery", &self.discovery.as_ref().map(|_| "[redacted]"))
    404             .field("policy_versions", &self.policy_versions)
    405             .field("paths", &"[redacted]")
    406             .finish()
    407     }
    408 }
    409 
    410 /// Stable source-free class for invalid Myc state metadata.
    411 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    412 pub enum MycStateMetadataErrorKind {
    413     Profile,
    414     Paths,
    415     Configuration,
    416     Identity,
    417     Database,
    418     Invariant,
    419 }
    420 
    421 /// Source-free Myc state-metadata construction failure.
    422 #[derive(Clone, Copy, PartialEq, Eq)]
    423 pub struct MycStateMetadataError {
    424     kind: MycStateMetadataErrorKind,
    425 }
    426 
    427 impl MycStateMetadataError {
    428     const fn new(kind: MycStateMetadataErrorKind) -> Self {
    429         Self { kind }
    430     }
    431 
    432     #[must_use]
    433     pub const fn kind(self) -> MycStateMetadataErrorKind {
    434         self.kind
    435     }
    436 }
    437 
    438 impl fmt::Display for MycStateMetadataError {
    439     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    440         formatter.write_str(match self.kind {
    441             MycStateMetadataErrorKind::Profile => "Myc configuration profile is inconsistent",
    442             MycStateMetadataErrorKind::Paths => "Myc state metadata paths are invalid",
    443             MycStateMetadataErrorKind::Configuration => {
    444                 "Myc normalized configuration identity is invalid"
    445             }
    446             MycStateMetadataErrorKind::Identity => "Myc expected identity binding is invalid",
    447             MycStateMetadataErrorKind::Database => "Myc database metadata is invalid",
    448             MycStateMetadataErrorKind::Invariant => "Myc metadata contract is invalid",
    449         })
    450     }
    451 }
    452 
    453 impl fmt::Debug for MycStateMetadataError {
    454     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    455         formatter
    456             .debug_struct("MycStateMetadataError")
    457             .field("kind", &self.kind)
    458             .finish()
    459     }
    460 }
    461 
    462 impl Error for MycStateMetadataError {}
    463 
    464 fn require_profile_binding(
    465     runtime: MycBootstrapProfileV1,
    466     configuration: MycConfigProfile,
    467 ) -> Result<(), MycStateMetadataError> {
    468     let matches = match runtime {
    469         MycBootstrapProfileV1::ServiceHost | MycBootstrapProfileV1::Interactive => {
    470             configuration == MycConfigProfile::Production
    471         }
    472         MycBootstrapProfileV1::RepoLocal => configuration == MycConfigProfile::RepoLocal,
    473     };
    474     matches
    475         .then_some(())
    476         .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Profile))
    477 }
    478 
    479 pub(crate) fn normalized_config_digest(
    480     profile: MycConfigProfile,
    481     normalized: &serde_json::Value,
    482 ) -> Result<MycNormalizedConfigDigest, MycStateMetadataError> {
    483     let bytes = serde_json::to_vec(normalized)
    484         .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Configuration))?;
    485     let length = u64::try_from(bytes.len())
    486         .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Configuration))?;
    487     let mut hasher = Sha256::new();
    488     hasher.update(NORMALIZED_CONFIG_DIGEST_DOMAIN);
    489     hasher.update([match profile {
    490         MycConfigProfile::Production => 0,
    491         MycConfigProfile::RepoLocal => 1,
    492     }]);
    493     hasher.update(length.to_be_bytes());
    494     hasher.update(bytes);
    495     Ok(MycNormalizedConfigDigest(hasher.finalize().into()))
    496 }
    497 
    498 fn governance_policies(
    499     normalized: &serde_json::Value,
    500 ) -> Result<MycGovernancePolicies, MycStateMetadataError> {
    501     let integer = |pointer: &str| {
    502         normalized
    503             .pointer(pointer)
    504             .and_then(serde_json::Value::as_u64)
    505             .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))
    506     };
    507     let policy = |class, name: &str| {
    508         let prefix = format!("/rate_limits/{name}");
    509         MycRateLimitPolicy::new(
    510             class,
    511             integer(&format!("{prefix}/window_ms"))?,
    512             u32::try_from(integer(&format!("{prefix}/max_attempts"))?)
    513                 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?,
    514             integer(&format!("{prefix}/retention_ms"))?,
    515             u32::try_from(integer(&format!("{prefix}/maximum_tracked_subjects"))?)
    516                 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?,
    517         )
    518         .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))
    519     };
    520     let relays = normalized
    521         .pointer("/relays")
    522         .and_then(serde_json::Value::as_array)
    523         .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?
    524         .iter()
    525         .map(|relay| {
    526             relay
    527                 .pointer("/id")
    528                 .and_then(serde_json::Value::as_str)
    529                 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))
    530                 .and_then(|id| {
    531                     MycRateRelayId::new(id).map_err(|_| {
    532                         MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant)
    533                     })
    534                 })
    535         })
    536         .collect::<Result<Vec<_>, _>>()?
    537         .into_boxed_slice();
    538     MycGovernancePolicies::new(
    539         policy(
    540             MycRateLimitClass::ConnectionAdmission,
    541             "connection_admission",
    542         )?,
    543         policy(MycRateLimitClass::ChallengeCreation, "challenge_creation")?,
    544         policy(
    545             MycRateLimitClass::ChallengeAuthorization,
    546             "challenge_authorization",
    547         )?,
    548         integer("/policy/retention/audit_ms")?,
    549         relays,
    550     )
    551     .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))
    552 }
    553 
    554 fn delivery_policies(
    555     normalized: &serde_json::Value,
    556 ) -> Result<MycDeliveryPolicies, MycStateMetadataError> {
    557     let invalid = || MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant);
    558     let integer = |pointer: &str| {
    559         normalized
    560             .pointer(pointer)
    561             .and_then(serde_json::Value::as_u64)
    562             .ok_or_else(invalid)
    563     };
    564     let mode = normalized
    565         .pointer("/transport/delivery_policy/mode")
    566         .and_then(serde_json::Value::as_str)
    567         .and_then(MycDeliveryPolicyMode::parse)
    568         .ok_or_else(invalid)?;
    569     let configured_quorum = normalized
    570         .pointer("/transport/delivery_policy/required_acknowledgements")
    571         .map(|value| {
    572             value
    573                 .as_u64()
    574                 .and_then(|number| u32::try_from(number).ok())
    575                 .ok_or_else(invalid)
    576         })
    577         .transpose()?;
    578     let targets = normalized
    579         .pointer("/relays")
    580         .and_then(serde_json::Value::as_array)
    581         .ok_or_else(invalid)?
    582         .iter()
    583         .filter(|relay| relay.pointer("/write").and_then(serde_json::Value::as_bool) == Some(true))
    584         .map(|relay| {
    585             let id = relay
    586                 .pointer("/id")
    587                 .and_then(serde_json::Value::as_str)
    588                 .ok_or_else(invalid)?;
    589             let required = relay
    590                 .pointer("/required")
    591                 .and_then(serde_json::Value::as_bool)
    592                 .ok_or_else(invalid)?;
    593             let id = MycDeliveryRelayId::new(id).map_err(|_| invalid())?;
    594             Ok((id, required))
    595         })
    596         .collect::<Result<Vec<_>, MycStateMetadataError>>()?;
    597     MycDeliveryPolicies::new(
    598         mode,
    599         configured_quorum,
    600         u32::try_from(integer("/transport/publish_retry/max_attempts")?).map_err(|_| invalid())?,
    601         integer("/transport/publish_retry/initial_backoff_ms")?,
    602         integer("/transport/publish_retry/maximum_backoff_ms")?,
    603         integer("/transport/publish_retry/attempt_deadline_ms")?,
    604         usize::try_from(integer("/resource_limits/queues/outbox")?).map_err(|_| invalid())?,
    605         targets,
    606     )
    607     .map_err(|_| invalid())
    608 }
    609 
    610 pub(crate) fn expected_identities(
    611     normalized: &serde_json::Value,
    612 ) -> Result<MycExpectedIdentities, MycStateMetadataError> {
    613     let identity = |pointer: &str| {
    614         normalized
    615             .pointer(pointer)
    616             .and_then(serde_json::Value::as_str)
    617             .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))
    618             .and_then(MycExpectedPublicIdentity::from_hex)
    619     };
    620     let discovery_enabled = normalized
    621         .pointer("/identity/discovery/enabled")
    622         .and_then(serde_json::Value::as_bool)
    623         .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?;
    624     Ok(MycExpectedIdentities {
    625         transport: identity("/identity/transport/expected_public_key")?,
    626         user: identity("/identity/user/expected_public_key")?,
    627         discovery: if discovery_enabled {
    628             Some(identity("/identity/discovery/binding/expected_public_key")?)
    629         } else {
    630             None
    631         },
    632     })
    633 }