commit da1a1c901fd1aa8028af33a2321dc7645aac2af0
parent 137d6e6902de01025bf2a4b404a4504dbb0dc85a
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 14:06:14 +0000
refactor(rhi): persist desired presence state
Diffstat:
13 files changed, 1917 insertions(+), 31 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -211,6 +211,15 @@
acknowledgement never proves delivery or failure.
- Keep profile and application-handler presence as deterministic durable desired
state with the same commit-before-I/O and exact-byte retry discipline.
+- Derive presence desired state only from one complete admitted configuration,
+ bind it to the latest durable configuration before mutation, and advance its
+ singleton generation only when the semantic presence authority changes.
+ Durable desired state contains only the closed mode, document-presence bits,
+ bounded target counts, queue capacity, and governed digests; it never stores
+ relay URLs, rendered or signed events, attempts, schedules, or outcomes.
+- Keep rendered and independently verified signed presence bytes, per-document
+ target state, attempt evidence, retry scheduling, and relay I/O with the next
+ ordered checkpoint. Never reconstruct exact committed bytes during retry.
## 7. Configuration, identity, state, and process boundaries
diff --git a/README b/README
@@ -343,6 +343,25 @@ outcomes, retry, recovery, and wave qualification. The exact machine contract
is
[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json).
+## Deterministic durable presence intent
+
+`RhiPresenceDesiredAuthority::from_config` derives the exact ordered
+service-profile and application-handler intent from one complete admitted
+configuration. It binds the verified service public identity, the configured
+relay-ID order and requiredness, and the bounded presence queue under separate
+domain-separated target-set and semantic desired-state digests. An independent
+validator re-derives that authority from the same complete configuration.
+
+Schema v9 stores only one sealed semantic desired-state snapshot. The first
+commit creates generation one, exact semantic replay performs no write, and a
+semantic change advances exactly one compare-and-swap generation. Every commit
+must still match the latest durable configuration binding. The table stores no
+relay URL, filesystem path, rendered event, signature, authored time, delivery
+attempt, or network result, and its triggers reject deletion and ungoverned
+updates. Rendering, signing, target delivery state, retries, and relay I/O remain
+owned by Step 205. The exact machine contract is
+[`presence_desired_state.v1.json`](contracts/services_hardening/presence_desired_state.v1.json).
+
## Atomic reconciliation finalization commit
`RhiReconciliationAttemptRepository::commit_finalization` borrows one sealed,
diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt
@@ -133,6 +133,26 @@ pub enum rhi::RhiPresenceCommandV1
pub rhi::RhiPresenceCommandV1::Desired
pub rhi::RhiPresenceCommandV1::Refresh
pub rhi::RhiPresenceCommandV1::Render
+pub enum rhi::RhiPresenceDesiredErrorKind
+pub rhi::RhiPresenceDesiredErrorKind::Binding
+pub rhi::RhiPresenceDesiredErrorKind::CommitOutcomeUnknown
+pub rhi::RhiPresenceDesiredErrorKind::InvalidConfiguration
+pub rhi::RhiPresenceDesiredErrorKind::InvalidMode
+pub rhi::RhiPresenceDesiredErrorKind::ResourceExhausted
+pub rhi::RhiPresenceDesiredErrorKind::Storage
+pub rhi::RhiPresenceDesiredErrorKind::TargetInventory
+impl rhi::RhiPresenceDesiredErrorKind
+pub const fn rhi::RhiPresenceDesiredErrorKind::code(self) -> &'static str
+pub enum rhi::RhiPresenceDesiredMode
+pub rhi::RhiPresenceDesiredMode::Disabled
+pub rhi::RhiPresenceDesiredMode::Enabled
+impl rhi::RhiPresenceDesiredMode
+pub const fn rhi::RhiPresenceDesiredMode::code(self) -> &'static str
+pub enum rhi::RhiPresenceDocumentKind
+pub rhi::RhiPresenceDocumentKind::ApplicationHandler
+pub rhi::RhiPresenceDocumentKind::ServiceProfile
+impl rhi::RhiPresenceDocumentKind
+pub const fn rhi::RhiPresenceDocumentKind::code(self) -> &'static str
pub enum rhi::RhiPublicationAttemptEvidenceErrorKind
pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber
pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal
@@ -620,6 +640,9 @@ impl core::fmt::Debug for rhi::RhiDecryptedIdentity
pub fn rhi::RhiDecryptedIdentity::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiDesiredPresenceRepository<'host>
impl rhi::RhiDesiredPresenceRepository<'_>
+pub async fn rhi::RhiDesiredPresenceRepository<'_>::commit(&self, &rhi::RhiPresenceDesiredAuthority) -> core::result::Result<rhi::RhiPresenceDesiredCommitOutcome, rhi::RhiPresenceDesiredError>
+pub async fn rhi::RhiDesiredPresenceRepository<'_>::current(&self) -> core::result::Result<core::option::Option<rhi::RhiPresenceDesiredState>, rhi::RhiPresenceDesiredError>
+impl rhi::RhiDesiredPresenceRepository<'_>
pub const fn rhi::RhiDesiredPresenceRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
pub const fn rhi::RhiDesiredPresenceRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiDesiredPresenceRepository<'_>
@@ -714,6 +737,50 @@ pub const fn rhi::RhiPreparedPublicationAttempt::exact_signed_event_bytes(&self)
pub fn rhi::RhiPreparedPublicationAttempt::relay_id(&self) -> &str
impl core::fmt::Debug for rhi::RhiPreparedPublicationAttempt
pub fn rhi::RhiPreparedPublicationAttempt::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPresenceDesiredAuthority
+impl rhi::RhiPresenceDesiredAuthority
+pub const fn rhi::RhiPresenceDesiredAuthority::desired_sha256(&self) -> &[u8; 32]
+pub fn rhi::RhiPresenceDesiredAuthority::document_kinds(&self) -> &[rhi::RhiPresenceDocumentKind]
+pub fn rhi::RhiPresenceDesiredAuthority::from_config(&rhi::RhiConfigDocumentV1) -> core::result::Result<Self, rhi::RhiPresenceDesiredError>
+pub const fn rhi::RhiPresenceDesiredAuthority::mode(&self) -> rhi::RhiPresenceDesiredMode
+pub const fn rhi::RhiPresenceDesiredAuthority::queue_capacity(&self) -> u32
+pub const fn rhi::RhiPresenceDesiredAuthority::target_set_sha256(&self) -> &[u8; 32]
+pub fn rhi::RhiPresenceDesiredAuthority::targets(&self) -> &[rhi::RhiPresenceTarget]
+impl core::fmt::Debug for rhi::RhiPresenceDesiredAuthority
+pub fn rhi::RhiPresenceDesiredAuthority::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPresenceDesiredCommitOutcome
+impl rhi::RhiPresenceDesiredCommitOutcome
+pub const fn rhi::RhiPresenceDesiredCommitOutcome::changed(self) -> bool
+pub const fn rhi::RhiPresenceDesiredCommitOutcome::state(self) -> rhi::RhiPresenceDesiredState
+pub struct rhi::RhiPresenceDesiredError
+impl rhi::RhiPresenceDesiredError
+pub const fn rhi::RhiPresenceDesiredError::code(self) -> &'static str
+pub const fn rhi::RhiPresenceDesiredError::kind(self) -> rhi::RhiPresenceDesiredErrorKind
+impl core::error::Error for rhi::RhiPresenceDesiredError
+impl core::fmt::Debug for rhi::RhiPresenceDesiredError
+pub fn rhi::RhiPresenceDesiredError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiPresenceDesiredError
+pub fn rhi::RhiPresenceDesiredError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPresenceDesiredState
+impl rhi::RhiPresenceDesiredState
+pub const fn rhi::RhiPresenceDesiredState::application_handler(self) -> bool
+pub const fn rhi::RhiPresenceDesiredState::desired_sha256(&self) -> &[u8; 32]
+pub const fn rhi::RhiPresenceDesiredState::generation(self) -> u64
+pub const fn rhi::RhiPresenceDesiredState::mode(self) -> rhi::RhiPresenceDesiredMode
+pub const fn rhi::RhiPresenceDesiredState::profile(self) -> bool
+pub const fn rhi::RhiPresenceDesiredState::queue_capacity(self) -> u32
+pub const fn rhi::RhiPresenceDesiredState::required_target_count(self) -> u8
+pub const fn rhi::RhiPresenceDesiredState::target_count(self) -> u8
+pub const fn rhi::RhiPresenceDesiredState::target_set_sha256(&self) -> &[u8; 32]
+impl core::fmt::Debug for rhi::RhiPresenceDesiredState
+pub fn rhi::RhiPresenceDesiredState::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPresenceTarget
+impl rhi::RhiPresenceTarget
+pub const fn rhi::RhiPresenceTarget::ordinal(&self) -> u8
+pub fn rhi::RhiPresenceTarget::relay_id(&self) -> &str
+pub const fn rhi::RhiPresenceTarget::required(&self) -> bool
+impl core::fmt::Debug for rhi::RhiPresenceTarget
+pub fn rhi::RhiPresenceTarget::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiProjectionRepository<'host>
impl rhi::RhiProjectionRepository<'_>
pub const fn rhi::RhiProjectionRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
@@ -1570,6 +1637,8 @@ pub const rhi::RHI_ENCRYPTED_IDENTITY_BACKUP_INCLUDED: bool
pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_CONTRACT_VERSION: u32
pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES: usize
pub const rhi::RHI_MIGRATION_CATALOG_SHA256: [u8; 32]
+pub const rhi::RHI_PRESENCE_DESIRED_CONTRACT_VERSION: u32
+pub const rhi::RHI_PRESENCE_DESIRED_MAX_TARGETS: usize
pub const rhi::RHI_PROVIDER_CONTRACT_VERSION: u32
pub const rhi::RHI_PUBLICATION_ATTEMPT_EVIDENCE_CONTRACT_VERSION: u32
pub const rhi::RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM: u16
@@ -1625,6 +1694,9 @@ pub const rhi::RHI_STATE_SCHEMA_VERSION_7_SHA256: [u8; 32]
pub const rhi::RHI_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256: [u8; 32]
pub const rhi::RHI_STATE_SCHEMA_VERSION_8_OBJECT_COUNT: u32
pub const rhi::RHI_STATE_SCHEMA_VERSION_8_SHA256: [u8; 32]
+pub const rhi::RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256: [u8; 32]
+pub const rhi::RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT: u32
+pub const rhi::RHI_STATE_SCHEMA_VERSION_9_SHA256: [u8; 32]
pub const rhi::RHI_STATUS_CONTRACT_VERSION: u32
pub const rhi::RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT: usize
pub const rhi::RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES: usize
@@ -1672,6 +1744,7 @@ pub fn rhi::rhi_schema_catalog() -> core::result::Result<radroots_service_sqlite
pub const fn rhi::rhi_state_repository_descriptors() -> &'static [rhi::RhiStateRepositoryDescriptor; 18]
pub async fn rhi::stage_rhi_state_restore(&rhi::RhiRuntimeContext, &rhi::RhiStateMetadata, rhi::RhiVerifiedStateBackup) -> core::result::Result<rhi::RhiStagedStateRestore, rhi::RhiStateMaintenanceError>
pub fn rhi::trade_mutation_subscription_kinds() -> alloc::vec::Vec<u32>
+pub fn rhi::validate_rhi_presence_desired_authority(&rhi::RhiConfigDocumentV1, &rhi::RhiPresenceDesiredAuthority) -> core::result::Result<(), rhi::RhiPresenceDesiredError>
pub fn rhi::validate_rhi_state_catalogs(&radroots_service_sqlite::migration::MigrationCatalog, &radroots_service_sqlite::integrity::catalog::SchemaCatalog) -> core::result::Result<(), rhi::RhiStateCatalogError>
pub fn rhi::verify_rhi_state_backup(&[u8], radroots_service_sqlite::backup::manifest::BackupManifestSha256, &std::path::Path, &rhi::RhiStateMetadata, core::num::nonzero::NonZeroU64) -> core::result::Result<rhi::RhiVerifiedStateBackup, rhi::RhiStateMaintenanceError>
pub type rhi::RhiReconciliationCoverage = radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1
diff --git a/contracts/services_hardening/presence_desired_state.v1.json b/contracts/services_hardening/presence_desired_state.v1.json
@@ -0,0 +1,94 @@
+{
+ "schema": "radroots.rhi.presence-desired-state",
+ "contract_version": 1,
+ "authority": {
+ "source": "complete_admitted_rhi_config_v1",
+ "modes": ["disabled", "enabled"],
+ "document_order": ["service_profile", "application_handler"],
+ "target_order": "configured_presence_target_relay_id_order",
+ "target_requiredness": "configured_relay_required_value",
+ "maximum_targets": 32,
+ "queue_capacity": {
+ "disabled": 0,
+ "enabled_minimum": 1,
+ "enabled_maximum": 4096
+ },
+ "independent_validation": "exact_rederivation_from_same_complete_config"
+ },
+ "target_set_digest": {
+ "hash": "sha256",
+ "domain_ascii_nul": "radroots.rhi.presence_target_set.v1\\0",
+ "framing": [
+ "target_count_u32_be",
+ "for_each_target_in_order:ordinal_u32_be",
+ "relay_id_utf8_length_u64_be",
+ "relay_id_exact_utf8",
+ "required_u8"
+ ]
+ },
+ "desired_state_digest": {
+ "hash": "sha256",
+ "domain_ascii_nul": "radroots.rhi.presence_desired_state.v1\\0",
+ "framing": [
+ "mode_u8_disabled_0_enabled_1",
+ "document_count_u32_be",
+ "ordered_document_kind_u8_profile_0_application_handler_1",
+ "target_count_u32_be",
+ "for_each_target_same_framing_as_target_set",
+ "queue_capacity_u32_be",
+ "service_public_key_utf8_length_u64_be",
+ "service_public_key_exact_lowerhex_utf8"
+ ],
+ "excludes": [
+ "configuration_fields_unrelated_to_presence",
+ "rendered_event",
+ "signature",
+ "authored_time",
+ "entropy",
+ "attempt",
+ "relay_url",
+ "network_outcome"
+ ]
+ },
+ "durable_state": {
+ "table": "presence_desired_state",
+ "write_class": "compare_and_swap",
+ "rows": "exactly_zero_or_one",
+ "first_generation": 1,
+ "maximum_generation": 9223372036854775807,
+ "exact_replay": "no_mutation_same_generation",
+ "semantic_change": "single_cas_generation_increment",
+ "delete": "forbidden",
+ "current_config_binding": "required_before_commit",
+ "read_bounds": "at_most_two_rows_with_bounded_blob_and_text_projection"
+ },
+ "commit_boundary": {
+ "before_rendering": true,
+ "before_signing": true,
+ "before_relay_io": true,
+ "commit_outcome_unknown": "typed_and_not_retried_as_uncommitted_without_reread"
+ },
+ "step_205_deferrals": [
+ "rendered_typed_events",
+ "exact_signed_bytes",
+ "per_document_target_state",
+ "delivery_attempts",
+ "retry_schedule",
+ "relay_io",
+ "observed_outcomes"
+ ],
+ "reference_vector": {
+ "config_fixture": "contracts/services_hardening/config.v1.example.toml",
+ "target_set_sha256": "959f04012841ae6e9bf3e109468b4f66cfa9d966aac1df36df09e45c1e1c48f9",
+ "desired_state_sha256": "7235f1e386e839427625dc364df7b51ee74d39d5f170e12b25cf2c42fd7731f0"
+ },
+ "effects": {
+ "sqlite": "typed_repository_commit_and_read_only",
+ "clock": false,
+ "entropy": false,
+ "filesystem": false,
+ "task_spawn": false,
+ "network": false,
+ "relay_io": false
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -8,6 +8,7 @@ mod config_v1;
mod features;
mod identity_credential;
mod identity_envelope;
+mod presence_desired;
mod publication;
mod publication_attempt;
mod publication_execution;
@@ -69,6 +70,12 @@ pub use identity_envelope::{
RhiIdentityRole, RhiWrappingCredential, open_rhi_encrypted_identity,
provision_rhi_encrypted_identity,
};
+pub use presence_desired::{
+ RHI_PRESENCE_DESIRED_CONTRACT_VERSION, RHI_PRESENCE_DESIRED_MAX_TARGETS,
+ RhiPresenceDesiredAuthority, RhiPresenceDesiredCommitOutcome, RhiPresenceDesiredError,
+ RhiPresenceDesiredErrorKind, RhiPresenceDesiredMode, RhiPresenceDesiredState,
+ RhiPresenceDocumentKind, RhiPresenceTarget, validate_rhi_presence_desired_authority,
+};
pub use publication::{
RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS,
RhiPublicationAuthority, RhiPublicationError, RhiPublicationErrorKind, RhiPublicationMode,
@@ -188,8 +195,9 @@ pub use state_catalog::{
RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT,
RHI_STATE_SCHEMA_VERSION_7_SHA256, RHI_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256,
RHI_STATE_SCHEMA_VERSION_8_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_8_SHA256,
- RhiStateCatalogError, RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
- validate_rhi_state_catalogs,
+ RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT,
+ RHI_STATE_SCHEMA_VERSION_9_SHA256, RhiStateCatalogError, RhiStateCatalogErrorKind,
+ rhi_migration_catalog, rhi_schema_catalog, validate_rhi_state_catalogs,
};
pub use state_config::{
RHI_CONFIG_BINDING_MAX_GENERATIONS, RhiConfigApplyError, RhiConfigApplyErrorKind,
diff --git a/src/presence_desired.rs b/src/presence_desired.rs
@@ -0,0 +1,1065 @@
+//! Deterministic durable desired state for RHI service presence.
+
+use core::fmt;
+use std::error::Error;
+
+use radroots_service_sqlite::{
+ ServiceSqliteTransaction, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind,
+};
+use serde_json::Value;
+use sha2::{Digest, Sha256};
+use sqlx::Row;
+
+use crate::{
+ RhiConfigDocumentV1, RhiDesiredPresenceRepository, RhiStateHostMode,
+ state_metadata::normalized_config_digest,
+};
+
+/// Exact version of the deterministic presence desired-state contract.
+pub const RHI_PRESENCE_DESIRED_CONTRACT_VERSION: u32 = 1;
+
+/// Maximum number of configured relay targets in one desired state.
+pub const RHI_PRESENCE_DESIRED_MAX_TARGETS: usize = 32;
+
+const TARGET_SET_DOMAIN: &[u8] = b"radroots.rhi.presence_target_set.v1\0";
+const DESIRED_STATE_DOMAIN: &[u8] = b"radroots.rhi.presence_desired_state.v1\0";
+
+const READ_CURRENT_CONFIG_SQL: &str = r#"SELECT
+ CASE WHEN typeof(normalized_config_sha256) = 'blob'
+ AND length(normalized_config_sha256) = 32
+ THEN normalized_config_sha256 ELSE NULL END AS normalized_config_sha256,
+ length(CAST(service_public_key AS BLOB)) AS service_public_key_bytes,
+ substr(service_public_key, 1, 65) AS service_public_key
+FROM rhi_config_bindings
+ORDER BY generation DESC
+LIMIT 1"#;
+
+const READ_DESIRED_SQL: &str = r#"SELECT singleton, generation,
+ enabled, profile, application_handler,
+ CASE WHEN typeof(target_set_sha256) = 'blob' AND length(target_set_sha256) = 32
+ THEN target_set_sha256 ELSE NULL END AS target_set_sha256,
+ target_count, required_target_count, queue_capacity,
+ CASE WHEN typeof(desired_sha256) = 'blob' AND length(desired_sha256) = 32
+ THEN desired_sha256 ELSE NULL END AS desired_sha256
+FROM presence_desired_state
+LIMIT 2"#;
+
+const INSERT_DESIRED_SQL: &str = r#"INSERT INTO presence_desired_state (
+ singleton, generation, enabled, profile, application_handler,
+ target_set_sha256, target_count, required_target_count,
+ queue_capacity, desired_sha256
+) VALUES (1, 1, ?, ?, ?, ?, ?, ?, ?, ?)"#;
+
+const UPDATE_DESIRED_SQL: &str = r#"UPDATE presence_desired_state
+SET generation = generation + 1,
+ enabled = ?, profile = ?, application_handler = ?,
+ target_set_sha256 = ?, target_count = ?, required_target_count = ?,
+ queue_capacity = ?, desired_sha256 = ?
+WHERE singleton = 1 AND generation = ? AND desired_sha256 = ?"#;
+
+/// Closed configured presence posture.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RhiPresenceDesiredMode {
+ Disabled,
+ Enabled,
+}
+
+impl RhiPresenceDesiredMode {
+ /// Returns the exact machine-contract spelling.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::Disabled => "disabled",
+ Self::Enabled => "enabled",
+ }
+ }
+}
+
+/// Closed ordered inventory of presence documents selected by configuration.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RhiPresenceDocumentKind {
+ ServiceProfile,
+ ApplicationHandler,
+}
+
+impl RhiPresenceDocumentKind {
+ /// Returns the exact machine-contract spelling.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::ServiceProfile => "service_profile",
+ Self::ApplicationHandler => "application_handler",
+ }
+ }
+}
+
+/// One immutable presence relay target derived from the admitted configuration.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct RhiPresenceTarget {
+ ordinal: u8,
+ relay_id: Box<str>,
+ required: bool,
+}
+
+impl RhiPresenceTarget {
+ /// Returns the stable zero-based target position.
+ #[must_use]
+ pub const fn ordinal(&self) -> u8 {
+ self.ordinal
+ }
+
+ /// Returns the validated stable relay identifier.
+ #[must_use]
+ pub fn relay_id(&self) -> &str {
+ &self.relay_id
+ }
+
+ /// Returns whether this relay is required by the admitted relay authority.
+ #[must_use]
+ pub const fn required(&self) -> bool {
+ self.required
+ }
+}
+
+impl fmt::Debug for RhiPresenceTarget {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPresenceTarget")
+ .field("ordinal", &self.ordinal)
+ .field("relay_id", &"[redacted]")
+ .field("required", &self.required)
+ .finish()
+ }
+}
+
+/// Sealed deterministic presence authority derived from one admitted config.
+///
+/// This value contains desired document kinds and stable relay authority only.
+/// It contains no rendered event, signature, delivery attempt, time, entropy,
+/// connection, or retry state.
+///
+/// ```compile_fail
+/// use rhi::RhiPresenceDesiredAuthority;
+///
+/// let _forged = RhiPresenceDesiredAuthority { mode: todo!() };
+/// ```
+#[derive(Clone, PartialEq, Eq)]
+pub struct RhiPresenceDesiredAuthority {
+ configuration_sha256: [u8; 32],
+ service_public_key: Box<str>,
+ mode: RhiPresenceDesiredMode,
+ document_kinds: Box<[RhiPresenceDocumentKind]>,
+ targets: Box<[RhiPresenceTarget]>,
+ queue_capacity: u32,
+ target_set_sha256: [u8; 32],
+ desired_sha256: [u8; 32],
+}
+
+impl RhiPresenceDesiredAuthority {
+ /// Derives the only presence desired-state authority from one admitted config.
+ pub fn from_config(config: &RhiConfigDocumentV1) -> Result<Self, RhiPresenceDesiredError> {
+ derive_authority(config.normalized(), config.profile())
+ }
+
+ /// Returns the explicit configured posture.
+ #[must_use]
+ pub const fn mode(&self) -> RhiPresenceDesiredMode {
+ self.mode
+ }
+
+ /// Returns the exact ordered desired-document inventory.
+ #[must_use]
+ pub fn document_kinds(&self) -> &[RhiPresenceDocumentKind] {
+ &self.document_kinds
+ }
+
+ /// Returns the exact ordered presence target inventory.
+ #[must_use]
+ pub fn targets(&self) -> &[RhiPresenceTarget] {
+ &self.targets
+ }
+
+ /// Returns the configured presence work-queue capacity, or zero when disabled.
+ #[must_use]
+ pub const fn queue_capacity(&self) -> u32 {
+ self.queue_capacity
+ }
+
+ /// Returns the domain-separated exact target-set identity.
+ #[must_use]
+ pub const fn target_set_sha256(&self) -> &[u8; 32] {
+ &self.target_set_sha256
+ }
+
+ /// Returns the domain-separated semantic desired-state identity.
+ #[must_use]
+ pub const fn desired_sha256(&self) -> &[u8; 32] {
+ &self.desired_sha256
+ }
+}
+
+impl fmt::Debug for RhiPresenceDesiredAuthority {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPresenceDesiredAuthority")
+ .field("mode", &self.mode)
+ .field("document_count", &self.document_kinds.len())
+ .field("target_count", &self.targets.len())
+ .field("queue_capacity", &self.queue_capacity)
+ .finish_non_exhaustive()
+ }
+}
+
+/// Independently re-derives and validates one desired-state authority.
+pub fn validate_rhi_presence_desired_authority(
+ config: &RhiConfigDocumentV1,
+ authority: &RhiPresenceDesiredAuthority,
+) -> Result<(), RhiPresenceDesiredError> {
+ let expected = RhiPresenceDesiredAuthority::from_config(config)?;
+ (expected == *authority)
+ .then_some(())
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::Binding))
+}
+
+/// One validated durable desired-state snapshot.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiPresenceDesiredState {
+ generation: u64,
+ mode: RhiPresenceDesiredMode,
+ profile: bool,
+ application_handler: bool,
+ target_set_sha256: [u8; 32],
+ target_count: u8,
+ required_target_count: u8,
+ queue_capacity: u32,
+ desired_sha256: [u8; 32],
+}
+
+impl RhiPresenceDesiredState {
+ /// Returns the monotonically committed desired-state generation.
+ #[must_use]
+ pub const fn generation(self) -> u64 {
+ self.generation
+ }
+
+ /// Returns the configured desired-state posture.
+ #[must_use]
+ pub const fn mode(self) -> RhiPresenceDesiredMode {
+ self.mode
+ }
+
+ /// Returns whether the service-profile document is desired.
+ #[must_use]
+ pub const fn profile(self) -> bool {
+ self.profile
+ }
+
+ /// Returns whether the application-handler document is desired.
+ #[must_use]
+ pub const fn application_handler(self) -> bool {
+ self.application_handler
+ }
+
+ /// Returns the target-set identity without exposing relay endpoints.
+ #[must_use]
+ pub const fn target_set_sha256(&self) -> &[u8; 32] {
+ &self.target_set_sha256
+ }
+
+ /// Returns the total configured target count.
+ #[must_use]
+ pub const fn target_count(self) -> u8 {
+ self.target_count
+ }
+
+ /// Returns the number of configured required targets.
+ #[must_use]
+ pub const fn required_target_count(self) -> u8 {
+ self.required_target_count
+ }
+
+ /// Returns the configured presence queue bound, or zero when disabled.
+ #[must_use]
+ pub const fn queue_capacity(self) -> u32 {
+ self.queue_capacity
+ }
+
+ /// Returns the semantic desired-state identity.
+ #[must_use]
+ pub const fn desired_sha256(&self) -> &[u8; 32] {
+ &self.desired_sha256
+ }
+}
+
+impl fmt::Debug for RhiPresenceDesiredState {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPresenceDesiredState")
+ .field("generation", &self.generation)
+ .field("mode", &self.mode)
+ .field("profile", &self.profile)
+ .field("application_handler", &self.application_handler)
+ .field("target_count", &self.target_count)
+ .field("required_target_count", &self.required_target_count)
+ .field("queue_capacity", &self.queue_capacity)
+ .field("digests", &"[redacted]")
+ .finish()
+ }
+}
+
+/// Result of one durable desired-state compare-and-swap operation.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct RhiPresenceDesiredCommitOutcome {
+ state: RhiPresenceDesiredState,
+ changed: bool,
+}
+
+impl RhiPresenceDesiredCommitOutcome {
+ /// Returns the exact committed state.
+ #[must_use]
+ pub const fn state(self) -> RhiPresenceDesiredState {
+ self.state
+ }
+
+ /// Returns whether this operation created a new durable generation.
+ #[must_use]
+ pub const fn changed(self) -> bool {
+ self.changed
+ }
+}
+
+/// Stable source-free desired-state failure classes.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiPresenceDesiredErrorKind {
+ InvalidConfiguration,
+ TargetInventory,
+ InvalidMode,
+ Binding,
+ ResourceExhausted,
+ Storage,
+ CommitOutcomeUnknown,
+}
+
+impl RhiPresenceDesiredErrorKind {
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidConfiguration => "presence_desired_configuration_invalid",
+ Self::TargetInventory => "presence_desired_target_inventory_invalid",
+ Self::InvalidMode => "presence_desired_mode_invalid",
+ Self::Binding => "presence_desired_binding_invalid",
+ Self::ResourceExhausted => "resource_exhausted",
+ Self::Storage => "presence_desired_storage_failed",
+ Self::CommitOutcomeUnknown => "presence_desired_commit_outcome_unknown",
+ }
+ }
+}
+
+/// Redacted source-free desired-state failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiPresenceDesiredError {
+ kind: RhiPresenceDesiredErrorKind,
+}
+
+impl RhiPresenceDesiredError {
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiPresenceDesiredErrorKind {
+ self.kind
+ }
+
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Display for RhiPresenceDesiredError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiPresenceDesiredErrorKind::InvalidConfiguration => {
+ "RHI presence desired-state configuration is invalid"
+ }
+ RhiPresenceDesiredErrorKind::TargetInventory => {
+ "RHI presence desired-state target inventory is invalid"
+ }
+ RhiPresenceDesiredErrorKind::InvalidMode => {
+ "RHI presence desired-state operation mode is invalid"
+ }
+ RhiPresenceDesiredErrorKind::Binding => "RHI presence desired-state binding is invalid",
+ RhiPresenceDesiredErrorKind::ResourceExhausted => {
+ "RHI presence desired-state capacity is exhausted"
+ }
+ RhiPresenceDesiredErrorKind::Storage => "RHI presence desired-state storage failed",
+ RhiPresenceDesiredErrorKind::CommitOutcomeUnknown => {
+ "RHI presence desired-state commit outcome is unknown"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiPresenceDesiredError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPresenceDesiredError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiPresenceDesiredError {}
+
+impl RhiDesiredPresenceRepository<'_> {
+ /// Commits one exact desired state before any presence rendering or relay I/O.
+ pub async fn commit(
+ &self,
+ authority: &RhiPresenceDesiredAuthority,
+ ) -> Result<RhiPresenceDesiredCommitOutcome, RhiPresenceDesiredError> {
+ require_writable(self)?;
+ let authority = authority.clone();
+ self.host()
+ .sqlite_host()
+ .transaction(move |transaction| {
+ Box::pin(async move { commit_desired(transaction, &authority).await })
+ })
+ .await
+ .map_err(map_transaction_error)
+ }
+
+ /// Reads the current validated desired-state snapshot without mutation.
+ pub async fn current(
+ &self,
+ ) -> Result<Option<RhiPresenceDesiredState>, RhiPresenceDesiredError> {
+ self.host()
+ .sqlite_host()
+ .transaction(move |transaction| {
+ Box::pin(async move { read_desired(transaction).await })
+ })
+ .await
+ .map_err(map_transaction_error)
+ }
+}
+
+fn derive_authority(
+ document: &Value,
+ profile: crate::RhiConfigProfile,
+) -> Result<RhiPresenceDesiredAuthority, RhiPresenceDesiredError> {
+ let configuration_sha256 = *normalized_config_digest(profile, document)
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))?
+ .as_bytes();
+ let service_public_key = document
+ .pointer("/identity/service/expected_public_key")
+ .and_then(Value::as_str)
+ .filter(|value| valid_public_key(value))
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))?;
+ let enabled = boolean(document, "/presence/enabled")?;
+ let profile_document = boolean(document, "/presence/profile")?;
+ let application_handler = boolean(document, "/presence/application_handler")?;
+ let configured_queue =
+ integer(document, "/resource_limits/queues/presence").and_then(|value| {
+ u32::try_from(value)
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))
+ })?;
+ if configured_queue == 0 || configured_queue > 4_096 {
+ return Err(failure(RhiPresenceDesiredErrorKind::InvalidConfiguration));
+ }
+
+ let mode = if enabled {
+ RhiPresenceDesiredMode::Enabled
+ } else {
+ RhiPresenceDesiredMode::Disabled
+ };
+ let mut document_kinds = Vec::with_capacity(2);
+ let (targets, queue_capacity) = match mode {
+ RhiPresenceDesiredMode::Disabled => {
+ if profile_document
+ || application_handler
+ || document.pointer("/presence/target_relay_ids").is_some()
+ {
+ return Err(failure(RhiPresenceDesiredErrorKind::InvalidConfiguration));
+ }
+ (Vec::new(), 0)
+ }
+ RhiPresenceDesiredMode::Enabled => {
+ if profile_document {
+ document_kinds.push(RhiPresenceDocumentKind::ServiceProfile);
+ }
+ if application_handler {
+ document_kinds.push(RhiPresenceDocumentKind::ApplicationHandler);
+ }
+ if document_kinds.is_empty() {
+ return Err(failure(RhiPresenceDesiredErrorKind::InvalidConfiguration));
+ }
+ (
+ derive_targets(document, "/presence/target_relay_ids")?,
+ configured_queue,
+ )
+ }
+ };
+ let target_set_sha256 = target_set_digest(&targets)?;
+ let desired_sha256 = desired_state_digest(
+ mode,
+ &document_kinds,
+ &targets,
+ queue_capacity,
+ service_public_key,
+ )?;
+ Ok(RhiPresenceDesiredAuthority {
+ configuration_sha256,
+ service_public_key: service_public_key.into(),
+ mode,
+ document_kinds: document_kinds.into_boxed_slice(),
+ targets: targets.into_boxed_slice(),
+ queue_capacity,
+ target_set_sha256,
+ desired_sha256,
+ })
+}
+
+fn derive_targets(
+ document: &Value,
+ pointer: &str,
+) -> Result<Vec<RhiPresenceTarget>, RhiPresenceDesiredError> {
+ let target_ids = document
+ .pointer(pointer)
+ .and_then(Value::as_array)
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::TargetInventory))?;
+ if target_ids.is_empty() || target_ids.len() > RHI_PRESENCE_DESIRED_MAX_TARGETS {
+ return Err(failure(RhiPresenceDesiredErrorKind::TargetInventory));
+ }
+ let relays = document
+ .pointer("/relays")
+ .and_then(Value::as_array)
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::TargetInventory))?;
+ let mut targets = Vec::with_capacity(target_ids.len());
+ for (ordinal, target_id) in target_ids.iter().enumerate() {
+ let relay_id = target_id
+ .as_str()
+ .filter(|value| valid_relay_id(value))
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::TargetInventory))?;
+ if targets
+ .iter()
+ .any(|target: &RhiPresenceTarget| target.relay_id() == relay_id)
+ {
+ return Err(failure(RhiPresenceDesiredErrorKind::TargetInventory));
+ }
+ let relay = relays
+ .iter()
+ .find(|relay| relay.pointer("/id").and_then(Value::as_str) == Some(relay_id))
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::TargetInventory))?;
+ if relay.pointer("/write").and_then(Value::as_bool) != Some(true) {
+ return Err(failure(RhiPresenceDesiredErrorKind::TargetInventory));
+ }
+ targets.push(RhiPresenceTarget {
+ ordinal: u8::try_from(ordinal)
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::TargetInventory))?,
+ relay_id: relay_id.into(),
+ required: relay
+ .pointer("/required")
+ .and_then(Value::as_bool)
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::TargetInventory))?,
+ });
+ }
+ Ok(targets)
+}
+
+fn target_set_digest(targets: &[RhiPresenceTarget]) -> Result<[u8; 32], RhiPresenceDesiredError> {
+ let mut digest = Sha256::new();
+ digest.update(TARGET_SET_DOMAIN);
+ digest.update(
+ u32::try_from(targets.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ for target in targets {
+ digest.update(u32::from(target.ordinal).to_be_bytes());
+ digest.update(
+ u64::try_from(target.relay_id.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ digest.update(target.relay_id.as_bytes());
+ digest.update([u8::from(target.required)]);
+ }
+ Ok(digest.finalize().into())
+}
+
+fn desired_state_digest(
+ mode: RhiPresenceDesiredMode,
+ document_kinds: &[RhiPresenceDocumentKind],
+ targets: &[RhiPresenceTarget],
+ queue_capacity: u32,
+ service_public_key: &str,
+) -> Result<[u8; 32], RhiPresenceDesiredError> {
+ let mut digest = Sha256::new();
+ digest.update(DESIRED_STATE_DOMAIN);
+ digest.update([match mode {
+ RhiPresenceDesiredMode::Disabled => 0,
+ RhiPresenceDesiredMode::Enabled => 1,
+ }]);
+ digest.update(
+ u32::try_from(document_kinds.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))?
+ .to_be_bytes(),
+ );
+ for kind in document_kinds {
+ digest.update([match kind {
+ RhiPresenceDocumentKind::ServiceProfile => 0,
+ RhiPresenceDocumentKind::ApplicationHandler => 1,
+ }]);
+ }
+ digest.update(
+ u32::try_from(targets.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ for target in targets {
+ digest.update(u32::from(target.ordinal).to_be_bytes());
+ digest.update(
+ u64::try_from(target.relay_id.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ digest.update(target.relay_id.as_bytes());
+ digest.update([u8::from(target.required)]);
+ }
+ digest.update(queue_capacity.to_be_bytes());
+ digest.update(
+ u64::try_from(service_public_key.len())
+ .map_err(|_| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))?
+ .to_be_bytes(),
+ );
+ digest.update(service_public_key.as_bytes());
+ Ok(digest.finalize().into())
+}
+
+fn require_writable(
+ repository: &RhiDesiredPresenceRepository<'_>,
+) -> Result<(), RhiPresenceDesiredError> {
+ if repository.host().mode() == RhiStateHostMode::ReadWriteExisting {
+ Ok(())
+ } else {
+ Err(failure(RhiPresenceDesiredErrorKind::InvalidMode))
+ }
+}
+
+async fn commit_desired(
+ transaction: &mut ServiceSqliteTransaction<'_>,
+ authority: &RhiPresenceDesiredAuthority,
+) -> Result<RhiPresenceDesiredCommitOutcome, OperationError> {
+ require_current_config(transaction, authority).await?;
+ let current = read_desired(transaction).await?;
+ if let Some(current) = current {
+ if matches_authority(current, authority) {
+ return Ok(RhiPresenceDesiredCommitOutcome {
+ state: current,
+ changed: false,
+ });
+ }
+ if current.generation == i64::MAX as u64 {
+ return Err(OperationError::ResourceExhausted);
+ }
+ let result = sqlx::query(UPDATE_DESIRED_SQL)
+ .bind(bool_i64(authority.mode == RhiPresenceDesiredMode::Enabled))
+ .bind(bool_i64(has_document(
+ authority,
+ RhiPresenceDocumentKind::ServiceProfile,
+ )))
+ .bind(bool_i64(has_document(
+ authority,
+ RhiPresenceDocumentKind::ApplicationHandler,
+ )))
+ .bind(authority.target_set_sha256.as_slice())
+ .bind(i64_count(authority.targets.len())?)
+ .bind(i64_count(
+ authority
+ .targets
+ .iter()
+ .filter(|target| target.required)
+ .count(),
+ )?)
+ .bind(i64::from(authority.queue_capacity))
+ .bind(authority.desired_sha256.as_slice())
+ .bind(i64_value(current.generation)?)
+ .bind(current.desired_sha256.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .map_err(|_| OperationError::Storage)?;
+ if result.rows_affected() != 1 {
+ return Err(OperationError::Binding);
+ }
+ } else {
+ let result = sqlx::query(INSERT_DESIRED_SQL)
+ .bind(bool_i64(authority.mode == RhiPresenceDesiredMode::Enabled))
+ .bind(bool_i64(has_document(
+ authority,
+ RhiPresenceDocumentKind::ServiceProfile,
+ )))
+ .bind(bool_i64(has_document(
+ authority,
+ RhiPresenceDocumentKind::ApplicationHandler,
+ )))
+ .bind(authority.target_set_sha256.as_slice())
+ .bind(i64_count(authority.targets.len())?)
+ .bind(i64_count(
+ authority
+ .targets
+ .iter()
+ .filter(|target| target.required)
+ .count(),
+ )?)
+ .bind(i64::from(authority.queue_capacity))
+ .bind(authority.desired_sha256.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .map_err(|_| OperationError::Storage)?;
+ if result.rows_affected() != 1 {
+ return Err(OperationError::Binding);
+ }
+ }
+ let committed = read_desired(transaction)
+ .await?
+ .filter(|state| matches_authority(*state, authority))
+ .ok_or(OperationError::Binding)?;
+ Ok(RhiPresenceDesiredCommitOutcome {
+ state: committed,
+ changed: true,
+ })
+}
+
+async fn require_current_config(
+ transaction: &mut ServiceSqliteTransaction<'_>,
+ authority: &RhiPresenceDesiredAuthority,
+) -> Result<(), OperationError> {
+ let rows = sqlx::query(READ_CURRENT_CONFIG_SQL)
+ .fetch_all(&mut *transaction)
+ .await
+ .map_err(|_| OperationError::Storage)?;
+ if rows.len() != 1 {
+ return Err(OperationError::Binding);
+ }
+ let row = &rows[0];
+ let configuration_sha256 = digest(row, "normalized_config_sha256")?;
+ let key_bytes = row
+ .try_get::<i64, _>("service_public_key_bytes")
+ .ok()
+ .and_then(|value| usize::try_from(value).ok())
+ .filter(|value| *value == 64)
+ .ok_or(OperationError::Binding)?;
+ let service_public_key = row
+ .try_get::<String, _>("service_public_key")
+ .map_err(|_| OperationError::Binding)?;
+ if service_public_key.len() != key_bytes
+ || !valid_public_key(&service_public_key)
+ || configuration_sha256 != authority.configuration_sha256
+ || service_public_key != authority.service_public_key.as_ref()
+ {
+ return Err(OperationError::Binding);
+ }
+ Ok(())
+}
+
+async fn read_desired(
+ transaction: &mut ServiceSqliteTransaction<'_>,
+) -> Result<Option<RhiPresenceDesiredState>, OperationError> {
+ let rows = sqlx::query(READ_DESIRED_SQL)
+ .fetch_all(&mut *transaction)
+ .await
+ .map_err(|_| OperationError::Storage)?;
+ match rows.as_slice() {
+ [] => Ok(None),
+ [row] => decode_desired(row).map(Some),
+ _ => Err(OperationError::Binding),
+ }
+}
+
+fn decode_desired(
+ row: &sqlx::sqlite::SqliteRow,
+) -> Result<RhiPresenceDesiredState, OperationError> {
+ if row.try_get::<i64, _>("singleton").ok() != Some(1) {
+ return Err(OperationError::Binding);
+ }
+ let generation = positive_u64(row, "generation")?;
+ let enabled = boolean_i64(row, "enabled")?;
+ let profile = boolean_i64(row, "profile")?;
+ let application_handler = boolean_i64(row, "application_handler")?;
+ let target_set_sha256 = digest(row, "target_set_sha256")?;
+ let target_count = count_u8(row, "target_count", RHI_PRESENCE_DESIRED_MAX_TARGETS)?;
+ let required_target_count = count_u8(row, "required_target_count", usize::from(target_count))?;
+ let queue_capacity = row
+ .try_get::<i64, _>("queue_capacity")
+ .ok()
+ .and_then(|value| u32::try_from(value).ok())
+ .filter(|value| *value <= 4_096)
+ .ok_or(OperationError::Binding)?;
+ let desired_sha256 = digest(row, "desired_sha256")?;
+ let valid = if enabled {
+ (profile || application_handler) && target_count > 0 && queue_capacity > 0
+ } else {
+ !profile
+ && !application_handler
+ && target_count == 0
+ && required_target_count == 0
+ && queue_capacity == 0
+ };
+ if !valid {
+ return Err(OperationError::Binding);
+ }
+ Ok(RhiPresenceDesiredState {
+ generation,
+ mode: if enabled {
+ RhiPresenceDesiredMode::Enabled
+ } else {
+ RhiPresenceDesiredMode::Disabled
+ },
+ profile,
+ application_handler,
+ target_set_sha256,
+ target_count,
+ required_target_count,
+ queue_capacity,
+ desired_sha256,
+ })
+}
+
+fn matches_authority(
+ state: RhiPresenceDesiredState,
+ authority: &RhiPresenceDesiredAuthority,
+) -> bool {
+ state.mode == authority.mode
+ && state.profile == has_document(authority, RhiPresenceDocumentKind::ServiceProfile)
+ && state.application_handler
+ == has_document(authority, RhiPresenceDocumentKind::ApplicationHandler)
+ && state.target_set_sha256 == authority.target_set_sha256
+ && usize::from(state.target_count) == authority.targets.len()
+ && usize::from(state.required_target_count)
+ == authority
+ .targets
+ .iter()
+ .filter(|target| target.required)
+ .count()
+ && state.queue_capacity == authority.queue_capacity
+ && state.desired_sha256 == authority.desired_sha256
+}
+
+fn has_document(authority: &RhiPresenceDesiredAuthority, kind: RhiPresenceDocumentKind) -> bool {
+ authority.document_kinds.contains(&kind)
+}
+
+fn digest(row: &sqlx::sqlite::SqliteRow, field: &str) -> Result<[u8; 32], OperationError> {
+ row.try_get::<Vec<u8>, _>(field)
+ .map_err(|_| OperationError::Binding)?
+ .try_into()
+ .map_err(|_| OperationError::Binding)
+}
+
+fn positive_u64(row: &sqlx::sqlite::SqliteRow, field: &str) -> Result<u64, OperationError> {
+ row.try_get::<i64, _>(field)
+ .ok()
+ .and_then(|value| u64::try_from(value).ok())
+ .filter(|value| *value != 0)
+ .ok_or(OperationError::Binding)
+}
+
+fn boolean_i64(row: &sqlx::sqlite::SqliteRow, field: &str) -> Result<bool, OperationError> {
+ match row.try_get::<i64, _>(field) {
+ Ok(0) => Ok(false),
+ Ok(1) => Ok(true),
+ Ok(_) | Err(_) => Err(OperationError::Binding),
+ }
+}
+
+fn count_u8(
+ row: &sqlx::sqlite::SqliteRow,
+ field: &str,
+ maximum: usize,
+) -> Result<u8, OperationError> {
+ row.try_get::<i64, _>(field)
+ .ok()
+ .and_then(|value| u8::try_from(value).ok())
+ .filter(|value| usize::from(*value) <= maximum)
+ .ok_or(OperationError::Binding)
+}
+
+fn bool_i64(value: bool) -> i64 {
+ i64::from(value)
+}
+
+fn i64_count(value: usize) -> Result<i64, OperationError> {
+ i64::try_from(value).map_err(|_| OperationError::InvalidInput)
+}
+
+fn i64_value(value: u64) -> Result<i64, OperationError> {
+ i64::try_from(value).map_err(|_| OperationError::ResourceExhausted)
+}
+
+fn boolean(document: &Value, pointer: &str) -> Result<bool, RhiPresenceDesiredError> {
+ document
+ .pointer(pointer)
+ .and_then(Value::as_bool)
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))
+}
+
+fn integer(document: &Value, pointer: &str) -> Result<u64, RhiPresenceDesiredError> {
+ document
+ .pointer(pointer)
+ .and_then(Value::as_u64)
+ .ok_or_else(|| failure(RhiPresenceDesiredErrorKind::InvalidConfiguration))
+}
+
+fn valid_public_key(value: &str) -> bool {
+ value.len() == 64
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
+ && nostr::PublicKey::from_hex(value).is_ok_and(|key| key.xonly().is_ok())
+}
+
+fn valid_relay_id(value: &str) -> bool {
+ !value.is_empty()
+ && value.len() <= 64
+ && value.as_bytes()[0].is_ascii_lowercase()
+ && value.bytes().all(|byte| {
+ byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
+ })
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum OperationError {
+ InvalidInput,
+ Binding,
+ ResourceExhausted,
+ Storage,
+}
+
+fn map_transaction_error(
+ error: ServiceSqliteTransactionError<OperationError>,
+) -> RhiPresenceDesiredError {
+ if error.kind() == ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown {
+ return failure(RhiPresenceDesiredErrorKind::CommitOutcomeUnknown);
+ }
+ failure(match error.operation_error().copied() {
+ Some(OperationError::InvalidInput) => RhiPresenceDesiredErrorKind::InvalidConfiguration,
+ Some(OperationError::Binding) => RhiPresenceDesiredErrorKind::Binding,
+ Some(OperationError::ResourceExhausted) => RhiPresenceDesiredErrorKind::ResourceExhausted,
+ Some(OperationError::Storage) | None => RhiPresenceDesiredErrorKind::Storage,
+ })
+}
+
+const fn failure(kind: RhiPresenceDesiredErrorKind) -> RhiPresenceDesiredError {
+ RhiPresenceDesiredError { kind }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{RhiConfigProfile, parse_rhi_config_v1};
+
+ const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+
+ fn config(source: &str) -> RhiConfigDocumentV1 {
+ parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("config")
+ }
+
+ #[test]
+ fn authority_is_deterministic_ordered_and_sealed() {
+ let config = config(EXAMPLE);
+ let first = RhiPresenceDesiredAuthority::from_config(&config).expect("authority");
+ let second = RhiPresenceDesiredAuthority::from_config(&config).expect("authority");
+ assert_eq!(first, second);
+ validate_rhi_presence_desired_authority(&config, &first).expect("independent validation");
+ assert_eq!(first.mode(), RhiPresenceDesiredMode::Enabled);
+ assert_eq!(
+ first.document_kinds(),
+ &[
+ RhiPresenceDocumentKind::ServiceProfile,
+ RhiPresenceDocumentKind::ApplicationHandler,
+ ]
+ );
+ assert_eq!(first.targets().len(), 2);
+ assert_eq!(first.targets()[0].ordinal(), 0);
+ assert_eq!(first.targets()[0].relay_id(), "relay-primary");
+ assert!(first.targets()[0].required());
+ assert_eq!(first.targets()[1].ordinal(), 1);
+ assert_eq!(first.targets()[1].relay_id(), "relay-secondary");
+ assert!(!first.targets()[1].required());
+ assert_eq!(first.queue_capacity(), 64);
+ assert_eq!(
+ first.target_set_sha256(),
+ &[
+ 0x95, 0x9f, 0x04, 0x01, 0x28, 0x41, 0xae, 0x6e, 0x9b, 0xf3, 0xe1, 0x09, 0x46, 0x8b,
+ 0x4f, 0x66, 0xcf, 0xa9, 0xd9, 0x66, 0xaa, 0xc1, 0xdf, 0x36, 0xdf, 0x09, 0xe4, 0x5c,
+ 0x1e, 0x1c, 0x48, 0xf9,
+ ]
+ );
+ assert_eq!(
+ first.desired_sha256(),
+ &[
+ 0x72, 0x35, 0xf1, 0xe3, 0x86, 0xe8, 0x39, 0x42, 0x76, 0x25, 0xdc, 0x36, 0x4d, 0xf7,
+ 0xb5, 0x1e, 0xe7, 0x4d, 0x39, 0xd5, 0xf1, 0x70, 0xe1, 0x2b, 0x25, 0xcf, 0x2c, 0x42,
+ 0xfd, 0x77, 0x31, 0xf0,
+ ]
+ );
+ let rendered = format!("{first:?} {:?}", first.targets()[0]);
+ assert!(!rendered.contains("relay-primary"));
+ assert!(!rendered.contains(&"2".repeat(64)));
+ }
+
+ #[test]
+ fn semantic_changes_change_only_the_deterministic_authority() {
+ let baseline =
+ RhiPresenceDesiredAuthority::from_config(&config(EXAMPLE)).expect("baseline");
+ for changed in [
+ EXAMPLE.replace("profile = true", "profile = false"),
+ EXAMPLE.replace(
+ "target_relay_ids = [\"relay-primary\", \"relay-secondary\"]",
+ "target_relay_ids = [\"relay-secondary\", \"relay-primary\"]",
+ ),
+ EXAMPLE.replacen("required = true", "required = false", 1),
+ EXAMPLE.replace("presence = 64", "presence = 63"),
+ EXAMPLE.replace(&"2".repeat(64), &"3".repeat(64)),
+ ] {
+ let changed = RhiPresenceDesiredAuthority::from_config(&config(&changed))
+ .expect("changed authority");
+ assert_ne!(changed.desired_sha256(), baseline.desired_sha256());
+ }
+ }
+
+ #[test]
+ fn disabled_authority_contains_no_document_target_or_queue_state() {
+ let disabled = EXAMPLE.replace(
+ "[presence]\nenabled = true\nprofile = true\napplication_handler = true\ntarget_relay_ids = [\"relay-primary\", \"relay-secondary\"]",
+ "[presence]\nenabled = false\nprofile = false\napplication_handler = false",
+ );
+ let authority = RhiPresenceDesiredAuthority::from_config(&config(&disabled))
+ .expect("disabled authority");
+ assert_eq!(authority.mode(), RhiPresenceDesiredMode::Disabled);
+ assert!(authority.document_kinds().is_empty());
+ assert!(authority.targets().is_empty());
+ assert_eq!(authority.queue_capacity(), 0);
+ }
+
+ #[test]
+ fn diagnostics_are_closed_source_free_and_redacted() {
+ for kind in [
+ RhiPresenceDesiredErrorKind::InvalidConfiguration,
+ RhiPresenceDesiredErrorKind::TargetInventory,
+ RhiPresenceDesiredErrorKind::InvalidMode,
+ RhiPresenceDesiredErrorKind::Binding,
+ RhiPresenceDesiredErrorKind::ResourceExhausted,
+ RhiPresenceDesiredErrorKind::Storage,
+ RhiPresenceDesiredErrorKind::CommitOutcomeUnknown,
+ ] {
+ let error = failure(kind);
+ assert_eq!(error.kind(), kind);
+ assert!(!error.code().is_empty());
+ assert!(Error::source(&error).is_none());
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains("wss://"));
+ assert!(!rendered.contains("relay-primary"));
+ assert!(!rendered.contains(&"2".repeat(64)));
+ }
+ }
+}
diff --git a/src/state_catalog.rs b/src/state_catalog.rs
@@ -12,7 +12,7 @@ use radroots_service_sqlite::{
pub const RHI_STATE_BASE_SCHEMA_VERSION: u32 = 1;
/// The newest governed RHI state schema understood by this binary.
-pub const RHI_STATE_SCHEMA_VERSION: u32 = 8;
+pub const RHI_STATE_SCHEMA_VERSION: u32 = 9;
/// The shared metadata and migration-ledger objects present at schema v1.
pub const RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT: u32 = 6;
@@ -38,10 +38,13 @@ pub const RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT: u32 = 63;
/// The shared objects plus fail-closed reconciliation-job shape guards.
pub const RHI_STATE_SCHEMA_VERSION_8_OBJECT_COUNT: u32 = 65;
+/// The shared objects plus deterministic durable presence desired state.
+pub const RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT: u32 = 69;
+
/// SHA-256 identity of the ordered migration catalog rooted at schema v1.
pub const RHI_MIGRATION_CATALOG_SHA256: [u8; 32] = [
- 0xe6, 0x5a, 0xd1, 0x15, 0x5c, 0x9d, 0xa2, 0x70, 0x32, 0x81, 0x99, 0x22, 0x68, 0x53, 0x0c, 0x87,
- 0xe5, 0x24, 0x8a, 0x52, 0xf7, 0x66, 0x01, 0x1c, 0x23, 0x5e, 0x1f, 0xdb, 0x67, 0x1c, 0x77, 0x4f,
+ 0x7f, 0x8c, 0x03, 0xb4, 0x81, 0x84, 0x08, 0xb5, 0x86, 0x74, 0x74, 0x12, 0xc2, 0x65, 0xac, 0x72,
+ 0xcd, 0x4d, 0xd8, 0x8a, 0x29, 0x0d, 0x8b, 0xbe, 0xe1, 0xd5, 0xf6, 0x8a, 0xdb, 0xf7, 0xaf, 0xd0,
];
/// SHA-256 identity of the exact schema-v1 object snapshot.
@@ -134,10 +137,22 @@ pub const RHI_STATE_SCHEMA_VERSION_8_SHA256: [u8; 32] = [
0xcc, 0xac, 0x90, 0x53, 0x8e, 0x0c, 0xd4, 0x9a, 0x4e, 0xe2, 0x14, 0xcb, 0x0a, 0x39, 0xc6, 0x18,
];
+/// SHA-256 identity of the schema-v9 presence desired-state migration.
+pub const RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256: [u8; 32] = [
+ 0x32, 0xda, 0xbe, 0x77, 0x72, 0x89, 0xe0, 0xfb, 0x6e, 0x64, 0xa4, 0xc1, 0xf8, 0x25, 0x78, 0x43,
+ 0xfc, 0x08, 0x01, 0x83, 0x21, 0xc3, 0xb7, 0xec, 0x5c, 0xa5, 0x84, 0xfa, 0x18, 0x62, 0xbd, 0xcc,
+];
+
+/// SHA-256 identity of the exact schema-v9 object snapshot.
+pub const RHI_STATE_SCHEMA_VERSION_9_SHA256: [u8; 32] = [
+ 0x55, 0x51, 0xe8, 0x79, 0x05, 0x44, 0xa7, 0xc7, 0x8c, 0x83, 0x76, 0xc5, 0xcc, 0xf8, 0x3d, 0xd2,
+ 0xa8, 0x48, 0x6d, 0x2d, 0xc0, 0x8b, 0x6a, 0x78, 0x08, 0xbb, 0x20, 0x07, 0xc9, 0x43, 0x35, 0xec,
+];
+
/// SHA-256 identity of the schema catalog bound to the migration catalog.
pub const RHI_STATE_SCHEMA_CATALOG_SHA256: [u8; 32] = [
- 0x93, 0x30, 0x35, 0x1d, 0x30, 0x0f, 0x70, 0x0f, 0x31, 0x7e, 0xd2, 0xc7, 0x2c, 0xbb, 0xb0, 0x85,
- 0x22, 0xa8, 0x27, 0xb9, 0x62, 0xfe, 0x6c, 0xcb, 0x34, 0x3e, 0x3e, 0xe7, 0x1f, 0xdf, 0xd0, 0x7c,
+ 0x5b, 0xea, 0x3e, 0x3e, 0xc6, 0xbe, 0x3f, 0x12, 0x49, 0xad, 0x19, 0xed, 0x7b, 0x2d, 0x2e, 0x87,
+ 0x2c, 0x34, 0x02, 0x55, 0x79, 0xa5, 0x99, 0xf2, 0x54, 0xde, 0x8e, 0x80, 0xcb, 0xa9, 0xb3, 0xc7,
];
macro_rules! rhi_config_bindings_table_sql {
@@ -750,6 +765,87 @@ const CREATE_RECONCILIATION_JOB_SHAPE_GUARDS_MIGRATION_SQL: &str = concat!(
";",
);
+macro_rules! presence_desired_state_table_sql {
+ () => {
+ r#"CREATE TABLE presence_desired_state (
+ singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
+ generation INTEGER NOT NULL
+ CHECK (generation BETWEEN 1 AND 9223372036854775807),
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
+ profile INTEGER NOT NULL CHECK (profile IN (0, 1)),
+ application_handler INTEGER NOT NULL CHECK (application_handler IN (0, 1)),
+ target_set_sha256 BLOB NOT NULL CHECK (length(target_set_sha256) = 32),
+ target_count INTEGER NOT NULL CHECK (target_count BETWEEN 0 AND 32),
+ required_target_count INTEGER NOT NULL
+ CHECK (required_target_count BETWEEN 0 AND target_count),
+ queue_capacity INTEGER NOT NULL CHECK (queue_capacity BETWEEN 0 AND 4096),
+ desired_sha256 BLOB NOT NULL UNIQUE CHECK (length(desired_sha256) = 32),
+ CHECK (
+ (enabled = 0 AND profile = 0 AND application_handler = 0
+ AND target_count = 0 AND required_target_count = 0
+ AND queue_capacity = 0)
+ OR
+ (enabled = 1 AND (profile = 1 OR application_handler = 1)
+ AND target_count BETWEEN 1 AND 32
+ AND queue_capacity BETWEEN 1 AND 4096)
+ )
+) STRICT"#
+ };
+}
+
+macro_rules! presence_desired_state_guard_insert_sql {
+ () => {
+ r#"CREATE TRIGGER presence_desired_state_guard_insert
+BEFORE INSERT ON presence_desired_state
+WHEN NEW.generation != 1
+ OR EXISTS (SELECT 1 FROM presence_desired_state)
+BEGIN
+ SELECT RAISE(ABORT, 'presence desired-state insertion is invalid');
+END"#
+ };
+}
+
+macro_rules! presence_desired_state_guard_update_sql {
+ () => {
+ r#"CREATE TRIGGER presence_desired_state_guard_update
+BEFORE UPDATE ON presence_desired_state
+WHEN NEW.singleton != OLD.singleton
+ OR OLD.generation >= 9223372036854775807
+ OR NEW.generation != OLD.generation + 1
+ OR NEW.desired_sha256 = OLD.desired_sha256
+BEGIN
+ SELECT RAISE(ABORT, 'presence desired-state transition is invalid');
+END"#
+ };
+}
+
+macro_rules! presence_desired_state_no_delete_sql {
+ () => {
+ r#"CREATE TRIGGER presence_desired_state_no_delete
+BEFORE DELETE ON presence_desired_state
+BEGIN
+ SELECT RAISE(ABORT, 'presence desired state is retained');
+END"#
+ };
+}
+
+const CREATE_PRESENCE_DESIRED_STATE_TABLE_SQL: &str = presence_desired_state_table_sql!();
+const CREATE_PRESENCE_DESIRED_STATE_GUARD_INSERT_SQL: &str =
+ presence_desired_state_guard_insert_sql!();
+const CREATE_PRESENCE_DESIRED_STATE_GUARD_UPDATE_SQL: &str =
+ presence_desired_state_guard_update_sql!();
+const CREATE_PRESENCE_DESIRED_STATE_NO_DELETE_SQL: &str = presence_desired_state_no_delete_sql!();
+const CREATE_PRESENCE_DESIRED_STATE_MIGRATION_SQL: &str = concat!(
+ presence_desired_state_table_sql!(),
+ ";\n",
+ presence_desired_state_guard_insert_sql!(),
+ ";\n",
+ presence_desired_state_guard_update_sql!(),
+ ";\n",
+ presence_desired_state_no_delete_sql!(),
+ ";",
+);
+
macro_rules! evidence_reconciliations_table_sql {
() => {
r#"CREATE TABLE evidence_reconciliations (
@@ -1538,6 +1634,22 @@ const TRADE_DIRTY_GENERATIONS_NO_DELETE_SHA256: [u8; 32] = [
0x36, 0x66, 0x3a, 0x1a, 0xd4, 0x65, 0x41, 0x9f, 0x23, 0x1e, 0xe6, 0xcd, 0x1e, 0xd1, 0x6d, 0xa4,
0x26, 0xac, 0x7d, 0x70, 0xde, 0xc3, 0x67, 0x75, 0x55, 0x62, 0xa8, 0x45, 0x52, 0x86, 0x54, 0x23,
];
+const PRESENCE_DESIRED_STATE_TABLE_SHA256: [u8; 32] = [
+ 0x78, 0x4d, 0xfc, 0x23, 0xd5, 0x05, 0xba, 0x09, 0xb3, 0x73, 0x76, 0xf0, 0x18, 0xaa, 0x03, 0xc6,
+ 0x3d, 0x98, 0x77, 0x1e, 0xba, 0xd6, 0x73, 0x25, 0x4a, 0x38, 0x46, 0xe3, 0x72, 0xc0, 0x80, 0x40,
+];
+const PRESENCE_DESIRED_STATE_GUARD_INSERT_SHA256: [u8; 32] = [
+ 0xe5, 0x55, 0xa5, 0x87, 0xa9, 0x73, 0x2f, 0xae, 0x7c, 0x2d, 0x4e, 0xde, 0xb1, 0x88, 0x93, 0x33,
+ 0x4a, 0xa4, 0x23, 0x12, 0x22, 0x7a, 0x71, 0xca, 0x6c, 0x2b, 0x38, 0x1f, 0x0b, 0x56, 0xad, 0x8b,
+];
+const PRESENCE_DESIRED_STATE_GUARD_UPDATE_SHA256: [u8; 32] = [
+ 0xb3, 0x2a, 0xc0, 0x44, 0xd6, 0x8c, 0x40, 0xfa, 0x3e, 0xbc, 0x57, 0x8a, 0x2d, 0x59, 0xcb, 0x1b,
+ 0xd6, 0x87, 0xd5, 0x3d, 0xa0, 0xc7, 0xec, 0x99, 0xb5, 0x1e, 0x63, 0xc9, 0x72, 0xeb, 0x27, 0x14,
+];
+const PRESENCE_DESIRED_STATE_NO_DELETE_SHA256: [u8; 32] = [
+ 0xca, 0xcd, 0xcf, 0x6f, 0x3b, 0x34, 0xc0, 0x7d, 0x6c, 0xf1, 0x1a, 0xab, 0x03, 0xd5, 0x19, 0x7c,
+ 0xca, 0x44, 0xab, 0x2e, 0x4f, 0x77, 0x65, 0x29, 0x75, 0x02, 0x4b, 0xbf, 0x1f, 0x04, 0xdc, 0x3f,
+];
/// Stable classes for invalid embedded RHI catalog definitions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1608,8 +1720,7 @@ impl fmt::Debug for RhiStateCatalogError {
impl Error for RhiStateCatalogError {}
-/// Constructs the exact ordered RHI migration catalog.
-pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError> {
+fn build_rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError> {
let configuration = MigrationDescriptor::sql(
2,
"create_configuration_binding_history",
@@ -1659,6 +1770,13 @@ pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError>
MigrationChecksum::from_bytes(RHI_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256),
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
+ let presence_desired_state = MigrationDescriptor::sql(
+ 9,
+ "create_presence_desired_state",
+ CREATE_PRESENCE_DESIRED_STATE_MIGRATION_SQL,
+ MigrationChecksum::from_bytes(RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
let catalog = MigrationCatalog::new([
configuration,
trade_evidence,
@@ -1667,10 +1785,17 @@ pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError>
reconciliation_results,
report_publication,
reconciliation_job_shape_guards,
+ presence_desired_state,
])
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
+ Ok(catalog)
+}
+
+/// Constructs the exact ordered RHI migration catalog.
+pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError> {
+ let catalog = build_rhi_migration_catalog()?;
if catalog.current_version() != RHI_STATE_SCHEMA_VERSION
- || catalog.descriptors().len() != 7
+ || catalog.descriptors().len() != 8
|| catalog.digest().as_bytes() != &RHI_MIGRATION_CATALOG_SHA256
{
return Err(RhiStateCatalogError::new(
@@ -1683,6 +1808,14 @@ pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError>
/// Constructs the exact RHI schema catalog bound to the migration catalog.
pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
let migrations = rhi_migration_catalog()?;
+ let catalog = build_rhi_schema_catalog(&migrations)?;
+ validate_rhi_state_catalogs(&migrations, &catalog)?;
+ Ok(catalog)
+}
+
+fn build_rhi_schema_catalog(
+ migrations: &MigrationCatalog,
+) -> Result<SchemaCatalog, RhiStateCatalogError> {
let version_one = SchemaVersionCatalog::new(
RHI_STATE_BASE_SCHEMA_VERSION,
[],
@@ -1726,13 +1859,19 @@ pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
let version_eight = SchemaVersionCatalog::new(
- RHI_STATE_SCHEMA_VERSION,
+ 8,
rhi_schema_version_eight_objects()?,
SchemaDigest::from_bytes(RHI_STATE_SCHEMA_VERSION_8_SHA256),
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
+ let version_nine = SchemaVersionCatalog::new(
+ RHI_STATE_SCHEMA_VERSION,
+ rhi_schema_version_nine_objects()?,
+ SchemaDigest::from_bytes(RHI_STATE_SCHEMA_VERSION_9_SHA256),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
let catalog = SchemaCatalog::new(
- &migrations,
+ migrations,
[
version_one,
version_two,
@@ -1742,10 +1881,10 @@ pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
version_six,
version_seven,
version_eight,
+ version_nine,
],
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
- validate_rhi_state_catalogs(&migrations, &catalog)?;
Ok(catalog)
}
@@ -1756,10 +1895,10 @@ pub fn validate_rhi_state_catalogs(
) -> Result<(), RhiStateCatalogError> {
let versions = schema.versions();
let valid = migrations.current_version() == RHI_STATE_SCHEMA_VERSION
- && migrations.descriptors().len() == 7
+ && migrations.descriptors().len() == 8
&& migrations.digest().as_bytes() == &RHI_MIGRATION_CATALOG_SHA256
&& schema.migration_catalog_digest() == migrations.digest()
- && versions.len() == 8
+ && versions.len() == 9
&& versions[0].version() == RHI_STATE_BASE_SCHEMA_VERSION
&& versions[0].object_count() == RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT
&& versions[0].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_1_SHA256
@@ -1781,9 +1920,12 @@ pub fn validate_rhi_state_catalogs(
&& versions[6].version() == 7
&& versions[6].object_count() == RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT
&& versions[6].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_7_SHA256
- && versions[7].version() == RHI_STATE_SCHEMA_VERSION
+ && versions[7].version() == 8
&& versions[7].object_count() == RHI_STATE_SCHEMA_VERSION_8_OBJECT_COUNT
&& versions[7].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_8_SHA256
+ && versions[8].version() == RHI_STATE_SCHEMA_VERSION
+ && versions[8].object_count() == RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT
+ && versions[8].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_9_SHA256
&& schema.digest().as_bytes() == &RHI_STATE_SCHEMA_CATALOG_SHA256;
if valid {
Ok(())
@@ -1867,6 +2009,51 @@ fn rhi_schema_version_eight_objects() -> Result<Vec<SchemaObject>, RhiStateCatal
Ok(objects)
}
+fn rhi_schema_version_nine_objects() -> Result<Vec<SchemaObject>, RhiStateCatalogError> {
+ let mut objects = rhi_schema_version_eight_objects()?;
+ objects.extend(rhi_presence_desired_state_objects()?);
+ Ok(objects)
+}
+
+fn rhi_presence_desired_state_objects() -> Result<[SchemaObject; 4], RhiStateCatalogError> {
+ let object = |kind, name, sql, digest| {
+ SchemaObject::new(
+ kind,
+ name,
+ "presence_desired_state",
+ sql,
+ SchemaDigest::from_bytes(digest),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))
+ };
+ Ok([
+ object(
+ SchemaObjectKind::Table,
+ "presence_desired_state",
+ CREATE_PRESENCE_DESIRED_STATE_TABLE_SQL,
+ PRESENCE_DESIRED_STATE_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "presence_desired_state_guard_insert",
+ CREATE_PRESENCE_DESIRED_STATE_GUARD_INSERT_SQL,
+ PRESENCE_DESIRED_STATE_GUARD_INSERT_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "presence_desired_state_guard_update",
+ CREATE_PRESENCE_DESIRED_STATE_GUARD_UPDATE_SQL,
+ PRESENCE_DESIRED_STATE_GUARD_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "presence_desired_state_no_delete",
+ CREATE_PRESENCE_DESIRED_STATE_NO_DELETE_SQL,
+ PRESENCE_DESIRED_STATE_NO_DELETE_SHA256,
+ )?,
+ ])
+}
+
fn rhi_reconciliation_job_shape_guard_objects() -> Result<[SchemaObject; 2], RhiStateCatalogError> {
let object = |name, sql, digest| {
SchemaObject::new(
diff --git a/src/state_repository.rs b/src/state_repository.rs
@@ -479,6 +479,12 @@ impl<'host> RhiPublicationOutboxRepository<'host> {
}
}
+impl<'host> RhiDesiredPresenceRepository<'host> {
+ pub(crate) const fn host(&self) -> &'host RhiStateHost {
+ self.host
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -11,6 +11,9 @@ const RUNTIME_ADAPTERS: &str = include_str!("../src/runtime_adapters.rs");
const RUNTIME_ADAPTER_CONTRACT: &str =
include_str!("../contracts/services_hardening/runtime_adapters.v1.json");
const RUNTIME_FOUNDATION: &str = include_str!("../src/runtime_foundation.rs");
+const PRESENCE_DESIRED: &str = include_str!("../src/presence_desired.rs");
+const PRESENCE_DESIRED_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/presence_desired_state.v1.json");
const PUBLICATION: &str = include_str!("../src/publication.rs");
const PUBLICATION_ATTEMPT: &str = include_str!("../src/publication_attempt.rs");
const PUBLICATION_ATTEMPT_CONTRACT: &str =
@@ -69,6 +72,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/features/trade_agreement_attestation.rs"),
include_str!("../src/identity_credential.rs"),
include_str!("../src/identity_envelope.rs"),
+ include_str!("../src/presence_desired.rs"),
include_str!("../src/publication.rs"),
include_str!("../src/publication_attempt.rs"),
include_str!("../src/publication_execution.rs"),
@@ -148,6 +152,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"features",
"identity_credential",
"identity_envelope",
+ "presence_desired",
"publication",
"publication_attempt",
"publication_execution",
@@ -197,6 +202,12 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"RhiStateCatalogError",
"RhiRuntimeAdapters",
"RhiPublicationAuthority",
+ "RhiPresenceDesiredAuthority",
+ "RhiPresenceDesiredCommitOutcome",
+ "RhiPresenceDesiredErrorKind",
+ "RhiPresenceDesiredState",
+ "validate_rhi_presence_desired_authority",
+ "RHI_PRESENCE_DESIRED_CONTRACT_VERSION",
"RhiPublicationErrorKind",
"RhiPublicationMode",
"RhiPublicationRetryPolicy",
@@ -306,6 +317,9 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
assert_eq!(public_modules, ["pub mod rhi"]);
assert!(PUBLIC_API.contains("pub struct rhi::NostrEventAdapter<'a>"));
assert!(PUBLIC_API.contains("pub struct rhi::TradeAgreementAttestationError"));
+ assert!(PUBLIC_API.contains("pub struct rhi::RhiPresenceDesiredAuthority"));
+ assert!(PUBLIC_API.contains("pub struct rhi::RhiPresenceDesiredState"));
+ assert!(PUBLIC_API.contains("pub enum rhi::RhiPresenceDesiredErrorKind"));
assert!(!PUBLIC_API.contains("rhi::adapters::"));
assert!(!PUBLIC_API.contains("rhi::features::"));
assert!(!PUBLIC_API.contains("rhi::runtime_adapters::"));
@@ -316,6 +330,47 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
}
#[test]
+fn presence_desired_state_is_config_bound_durable_and_effect_free() {
+ let contract: serde_json::Value =
+ serde_json::from_str(PRESENCE_DESIRED_CONTRACT).expect("presence-desired-state contract");
+ assert_eq!(contract["schema"], "radroots.rhi.presence-desired-state");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["authority"]["maximum_targets"], 32);
+ assert_eq!(contract["durable_state"]["write_class"], "compare_and_swap");
+ assert_eq!(contract["durable_state"]["rows"], "exactly_zero_or_one");
+ assert_eq!(contract["effects"]["network"], false);
+ assert_eq!(contract["effects"]["relay_io"], false);
+ for required in [
+ "pub fn validate_rhi_presence_desired_authority(",
+ "require_current_config(transaction, authority).await?",
+ "LIMIT 1",
+ "LIMIT 2",
+ "ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown",
+ ] {
+ assert!(
+ PRESENCE_DESIRED.contains(required),
+ "presence desired-state boundary is missing {required}"
+ );
+ }
+ for forbidden in [
+ "SystemTime",
+ "OsRng",
+ "thread_rng",
+ "tokio::spawn",
+ "std::net",
+ "EventSink",
+ "sign_nostr_event",
+ ] {
+ assert!(
+ !PRESENCE_DESIRED.contains(forbidden),
+ "presence desired-state boundary gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(!ROOT.contains("pub mod presence_desired"));
+ assert!(!PUBLIC_API.contains("rhi::presence_desired::"));
+}
+
+#[test]
fn publication_authority_is_config_derived_sealed_and_effect_free() {
let contract: serde_json::Value =
serde_json::from_str(PUBLICATION_CONTRACT).expect("publication contract");
@@ -799,7 +854,7 @@ fn public_errors_are_crate_owned_redacted_and_source_free() {
.lines()
.filter(|line| line.starts_with("pub struct rhi::") && line.ends_with("Error"))
.count();
- assert_eq!(public_error_count, 29);
+ assert_eq!(public_error_count, 30);
}
#[test]
@@ -1233,6 +1288,8 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"## Canonical signed reconciliation attestation",
"[`reconciliation_attestation.v1.json`](contracts/services_hardening/reconciliation_attestation.v1.json)",
"## Explicit publication authority and durable schema",
+ "## Deterministic durable presence intent",
+ "[`presence_desired_state.v1.json`](contracts/services_hardening/presence_desired_state.v1.json)",
"[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)",
"## Bounded publication attempt evidence",
"[`publication_attempt_evidence.v1.json`](contracts/services_hardening/publication_attempt_evidence.v1.json)",
diff --git a/tests/services_hardening_presence_desired_state.rs b/tests/services_hardening_presence_desired_state.rs
@@ -0,0 +1,325 @@
+#![forbid(unsafe_code)]
+#![cfg(any(target_os = "linux", target_os = "macos"))]
+
+use std::{fs, os::unix::fs::PermissionsExt, path::Path};
+
+use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
+use radroots_storage::event::SourceGeneration;
+use rhi::{
+ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigDocumentV1,
+ RhiConfigProfile, RhiPresenceDesiredAuthority, RhiPresenceDesiredErrorKind,
+ RhiPresenceDesiredMode, RhiStateMetadata, apply_rhi_configuration, initialize_rhi_state,
+ open_rhi_state_inspection, open_rhi_state_read_write_from_config, parse_rhi_cli_v1_from,
+ parse_rhi_config_v1, resolve_rhi_runtime_context, validate_rhi_presence_desired_authority,
+};
+use sqlx::{ConnectOptions, Connection, Row, SqliteConnection, sqlite::SqliteConnectOptions};
+
+const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+const CONTRACT: &str =
+ include_str!("../contracts/services_hardening/presence_desired_state.v1.json");
+const SOURCE: &str = include_str!("../src/presence_desired.rs");
+
+fn runtime(root: &Path) -> rhi::RhiRuntimeContext {
+ let invocation = parse_rhi_cli_v1_from([
+ "rhi",
+ "--profile",
+ "repo-local",
+ "--instance",
+ "primary",
+ "--repo-local-root",
+ root.to_str().expect("UTF-8 root"),
+ "run",
+ ])
+ .expect("invocation");
+ resolve_rhi_runtime_context(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ &invocation,
+ )
+ .expect("runtime")
+}
+
+fn config(source: &str) -> RhiConfigDocumentV1 {
+ parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("configuration")
+}
+
+fn evidence(at: u64) -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) {
+ let applied_at = MigrationAppliedAtUnixSeconds::new(at).expect("migration time");
+ let build = MigrationBuildIdentity::new(
+ env!("CARGO_PKG_VERSION"),
+ "1111111111111111111111111111111111111111",
+ "21b11e7a5120ea949f7ad0838c746873fc73aac2",
+ "rustc-test",
+ "test-target",
+ "service-host",
+ 1,
+ rhi::RHI_STATE_SCHEMA_VERSION,
+ 1,
+ 1,
+ 1,
+ )
+ .expect("build identity");
+ (applied_at, build)
+}
+
+async fn offline_connection(runtime: &rhi::RhiRuntimeContext) -> SqliteConnection {
+ let options = SqliteConnectOptions::new()
+ .filename(runtime.artifacts().state_database())
+ .create_if_missing(false)
+ .foreign_keys(false)
+ .disable_statement_logging();
+ SqliteConnection::connect_with(&options)
+ .await
+ .expect("offline connection")
+}
+
+#[tokio::test]
+async fn desired_state_is_durable_exact_replay_and_semantic_change_only() {
+ let directory = tempfile::tempdir().expect("root");
+ let runtime = runtime(directory.path());
+ fs::create_dir_all(runtime.context().paths().state()).expect("state directory");
+ fs::set_permissions(
+ runtime.context().paths().state(),
+ fs::Permissions::from_mode(0o700),
+ )
+ .expect("state mode");
+ let original = config(EXAMPLE);
+ let metadata = RhiStateMetadata::new(
+ &runtime,
+ &original,
+ SourceGeneration::new([0x5a; 32]).expect("source generation"),
+ 1_725_000_000_000,
+ )
+ .expect("metadata");
+ let (first_at, first_build) = evidence(1_725_000_000);
+ initialize_rhi_state(&runtime, &metadata, first_at, &first_build)
+ .await
+ .expect("initialize");
+
+ let original_authority =
+ RhiPresenceDesiredAuthority::from_config(&original).expect("authority");
+ let host = open_rhi_state_read_write_from_config(&runtime, &original, first_at, &first_build)
+ .await
+ .expect("writer");
+ let repository = host.repositories().desired_presence();
+ assert_eq!(repository.current().await.expect("initial read"), None);
+ let first = repository
+ .commit(&original_authority)
+ .await
+ .expect("first commit");
+ assert!(first.changed());
+ assert_eq!(first.state().generation(), 1);
+ assert_eq!(first.state().mode(), RhiPresenceDesiredMode::Enabled);
+ assert!(first.state().profile());
+ assert!(first.state().application_handler());
+ assert_eq!(first.state().target_count(), 2);
+ assert_eq!(first.state().required_target_count(), 1);
+ assert_eq!(first.state().queue_capacity(), 64);
+ let replay = repository
+ .commit(&original_authority)
+ .await
+ .expect("exact replay");
+ assert!(!replay.changed());
+ assert_eq!(replay.state(), first.state());
+ assert_eq!(
+ repository.current().await.expect("current"),
+ Some(first.state())
+ );
+ host.close().await.expect("writer close");
+
+ let inspection = open_rhi_state_inspection(&runtime, &metadata)
+ .await
+ .expect("inspection");
+ let inspected = inspection
+ .repositories()
+ .desired_presence()
+ .current()
+ .await
+ .expect("inspection read");
+ assert_eq!(inspected, Some(first.state()));
+ let rejected = inspection
+ .repositories()
+ .desired_presence()
+ .commit(&original_authority)
+ .await
+ .expect_err("inspection mutation");
+ assert_eq!(rejected.kind(), RhiPresenceDesiredErrorKind::InvalidMode);
+ inspection.close().await.expect("inspection close");
+
+ let unrelated_source = EXAMPLE.replacen("level = \"info\"", "level = \"debug\"", 1);
+ let unrelated = config(&unrelated_source);
+ let unrelated_authority =
+ RhiPresenceDesiredAuthority::from_config(&unrelated).expect("unrelated authority");
+ assert_eq!(
+ unrelated_authority.desired_sha256(),
+ original_authority.desired_sha256()
+ );
+ assert_eq!(
+ validate_rhi_presence_desired_authority(&unrelated, &original_authority)
+ .expect_err("full configuration binding")
+ .kind(),
+ RhiPresenceDesiredErrorKind::Binding
+ );
+ let (second_at, second_build) = evidence(1_725_000_001);
+ apply_rhi_configuration(&runtime, &original, &unrelated, second_at, &second_build)
+ .await
+ .expect("apply unrelated configuration");
+ let host =
+ open_rhi_state_read_write_from_config(&runtime, &unrelated, second_at, &second_build)
+ .await
+ .expect("writer after unrelated config");
+ let replay = host
+ .repositories()
+ .desired_presence()
+ .commit(&unrelated_authority)
+ .await
+ .expect("semantic replay");
+ assert!(!replay.changed());
+ assert_eq!(replay.state().generation(), 1);
+ host.close().await.expect("writer close");
+
+ let presence_source = unrelated_source.replace("profile = true", "profile = false");
+ let presence_changed = config(&presence_source);
+ let changed_authority =
+ RhiPresenceDesiredAuthority::from_config(&presence_changed).expect("changed authority");
+ assert_ne!(
+ changed_authority.desired_sha256(),
+ original_authority.desired_sha256()
+ );
+ let (third_at, third_build) = evidence(1_725_000_002);
+ apply_rhi_configuration(
+ &runtime,
+ &unrelated,
+ &presence_changed,
+ third_at,
+ &third_build,
+ )
+ .await
+ .expect("apply presence configuration");
+ let host =
+ open_rhi_state_read_write_from_config(&runtime, &presence_changed, third_at, &third_build)
+ .await
+ .expect("writer after presence change");
+ let stale = host
+ .repositories()
+ .desired_presence()
+ .commit(&original_authority)
+ .await
+ .expect_err("stale config authority");
+ assert_eq!(stale.kind(), RhiPresenceDesiredErrorKind::Binding);
+ let changed = host
+ .repositories()
+ .desired_presence()
+ .commit(&changed_authority)
+ .await
+ .expect("changed desired state");
+ assert!(changed.changed());
+ assert_eq!(changed.state().generation(), 2);
+ assert!(!changed.state().profile());
+ assert!(changed.state().application_handler());
+ assert_eq!(changed.state().target_count(), 2);
+ assert_eq!(
+ changed.state().desired_sha256(),
+ changed_authority.desired_sha256()
+ );
+ host.close().await.expect("final writer close");
+
+ let mut connection = offline_connection(&runtime).await;
+ let row = sqlx::query(
+ "SELECT COUNT(*) AS row_count, generation, length(desired_sha256) AS digest_bytes \
+ FROM presence_desired_state",
+ )
+ .fetch_one(&mut connection)
+ .await
+ .expect("durable desired state");
+ assert_eq!(row.try_get::<i64, _>("row_count").unwrap(), 1);
+ assert_eq!(row.try_get::<i64, _>("generation").unwrap(), 2);
+ assert_eq!(row.try_get::<i64, _>("digest_bytes").unwrap(), 32);
+ assert!(
+ sqlx::query("UPDATE presence_desired_state SET generation = generation")
+ .execute(&mut connection)
+ .await
+ .is_err()
+ );
+ assert!(
+ sqlx::query("DELETE FROM presence_desired_state")
+ .execute(&mut connection)
+ .await
+ .is_err()
+ );
+ connection.close().await.expect("offline close");
+
+ let database = fs::read(runtime.artifacts().state_database()).expect("database bytes");
+ for forbidden in [
+ b"wss://relay-primary.example".as_slice(),
+ b"relay-primary".as_slice(),
+ directory.path().to_string_lossy().as_bytes(),
+ ] {
+ assert!(
+ !database
+ .windows(forbidden.len())
+ .any(|window| window == forbidden)
+ );
+ }
+}
+
+#[test]
+fn machine_contract_freezes_step_204_and_defers_step_205_effects() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
+ assert_eq!(contract["schema"], "radroots.rhi.presence-desired-state");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["authority"]["maximum_targets"], 32);
+ assert_eq!(
+ contract["reference_vector"]["target_set_sha256"],
+ "959f04012841ae6e9bf3e109468b4f66cfa9d966aac1df36df09e45c1e1c48f9"
+ );
+ assert_eq!(
+ contract["reference_vector"]["desired_state_sha256"],
+ "7235f1e386e839427625dc364df7b51ee74d39d5f170e12b25cf2c42fd7731f0"
+ );
+ assert_eq!(contract["effects"]["clock"], false);
+ assert_eq!(contract["effects"]["entropy"], false);
+ assert_eq!(contract["effects"]["network"], false);
+ assert_eq!(contract["effects"]["relay_io"], false);
+ assert_eq!(contract["step_205_deferrals"].as_array().unwrap().len(), 7);
+ for required in [
+ "DESIRED_STATE_DOMAIN",
+ "TARGET_SET_DOMAIN",
+ "pub fn validate_rhi_presence_desired_authority(",
+ "pub async fn commit(",
+ "pub async fn current(",
+ "require_current_config(transaction, authority).await?",
+ "LIMIT 2",
+ "ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown",
+ ] {
+ assert!(
+ SOURCE.contains(required),
+ "missing Step204 boundary {required}"
+ );
+ }
+ for forbidden in [
+ "SystemTime",
+ "OsRng",
+ "thread_rng",
+ "tokio::spawn",
+ "std::net",
+ "NostrEventAdapter",
+ "sign_nostr_event",
+ ] {
+ assert!(
+ !SOURCE.contains(forbidden),
+ "unexpected Step205 effect {forbidden}"
+ );
+ }
+ for kind in [
+ RhiPresenceDesiredErrorKind::InvalidConfiguration,
+ RhiPresenceDesiredErrorKind::TargetInventory,
+ RhiPresenceDesiredErrorKind::InvalidMode,
+ RhiPresenceDesiredErrorKind::Binding,
+ RhiPresenceDesiredErrorKind::ResourceExhausted,
+ RhiPresenceDesiredErrorKind::Storage,
+ RhiPresenceDesiredErrorKind::CommitOutcomeUnknown,
+ ] {
+ let rendered = format!("{kind:?}");
+ assert!(!rendered.contains("relay-primary"));
+ }
+}
diff --git a/tests/services_hardening_state_catalog.rs b/tests/services_hardening_state_catalog.rs
@@ -20,8 +20,9 @@ use rhi::{
RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT,
RHI_STATE_SCHEMA_VERSION_7_SHA256, RHI_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256,
RHI_STATE_SCHEMA_VERSION_8_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_8_SHA256,
- RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
- validate_rhi_state_catalogs,
+ RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT,
+ RHI_STATE_SCHEMA_VERSION_9_SHA256, RhiStateCatalogErrorKind, rhi_migration_catalog,
+ rhi_schema_catalog, validate_rhi_state_catalogs,
};
const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
@@ -29,14 +30,14 @@ const LIB_SOURCE: &str = include_str!("../src/lib.rs");
const MANIFEST: &str = include_str!("../Cargo.toml");
#[test]
-fn schema_v1_through_v8_catalogs_have_exact_literal_identities() {
+fn schema_v1_through_v9_catalogs_have_exact_literal_identities() {
let migrations = rhi_migration_catalog().expect("RHI migration catalog");
let schema = rhi_schema_catalog().expect("RHI schema catalog");
assert_eq!(RHI_STATE_BASE_SCHEMA_VERSION, 1);
- assert_eq!(RHI_STATE_SCHEMA_VERSION, 8);
- assert_eq!(migrations.descriptors().len(), 7);
- assert_eq!(migrations.current_version(), 8);
+ assert_eq!(RHI_STATE_SCHEMA_VERSION, 9);
+ assert_eq!(migrations.descriptors().len(), 8);
+ assert_eq!(migrations.current_version(), 9);
assert_eq!(migrations.descriptors()[0].target_version(), 2);
assert_eq!(
migrations.descriptors()[0].name().as_str(),
@@ -100,12 +101,21 @@ fn schema_v1_through_v8_catalogs_have_exact_literal_identities() {
migrations.descriptors()[6].checksum().as_bytes(),
&RHI_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256
);
+ assert_eq!(migrations.descriptors()[7].target_version(), 9);
+ assert_eq!(
+ migrations.descriptors()[7].name().as_str(),
+ "create_presence_desired_state"
+ );
+ assert_eq!(
+ migrations.descriptors()[7].checksum().as_bytes(),
+ &RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256
+ );
assert_eq!(
migrations.digest().as_bytes(),
&RHI_MIGRATION_CATALOG_SHA256
);
- assert_eq!(schema.versions().len(), 8);
+ assert_eq!(schema.versions().len(), 9);
let version = schema.versions()[0];
assert_eq!(version.version(), 1);
assert_eq!(
@@ -194,13 +204,24 @@ fn schema_v1_through_v8_catalogs_have_exact_literal_identities() {
version.digest().as_bytes(),
&RHI_STATE_SCHEMA_VERSION_8_SHA256
);
+ let version = schema.versions()[8];
+ assert_eq!(version.version(), 9);
+ assert_eq!(
+ version.object_count(),
+ RHI_STATE_SCHEMA_VERSION_9_OBJECT_COUNT
+ );
+ assert_eq!(version.object_count(), 69);
+ assert_eq!(
+ version.digest().as_bytes(),
+ &RHI_STATE_SCHEMA_VERSION_9_SHA256
+ );
assert_eq!(schema.digest().as_bytes(), &RHI_STATE_SCHEMA_CATALOG_SHA256);
assert_eq!(schema.migration_catalog_digest(), migrations.digest());
validate_rhi_state_catalogs(&migrations, &schema).expect("exact catalogs");
assert_eq!(
lower_hex(&RHI_MIGRATION_CATALOG_SHA256),
- "e65ad1155c9da2703281992268530c87e5248a52f766011c235e1fdb671c774f"
+ "7f8c03b4818408b586747412c265ac72cd4dd88a290d8bbee1d5f68adbf7afd0"
);
assert_eq!(
lower_hex(&RHI_STATE_SCHEMA_VERSION_1_SHA256),
@@ -263,8 +284,16 @@ fn schema_v1_through_v8_catalogs_have_exact_literal_identities() {
"7cef559ae1e6efe158c5d1de50114ebaccac90538e0cd49a4ee214cb0a39c618"
);
assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256),
+ "32dabe777289e0fb6e64a4c1f8257843fc08018321c3b7ec5ca584fa1862bdcc"
+ );
+ assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_VERSION_9_SHA256),
+ "5551e8790544a7c78c8376c5ccf83dd2a8486d2dc08b6a7808bb2007c94335ec"
+ );
+ assert_eq!(
lower_hex(&RHI_STATE_SCHEMA_CATALOG_SHA256),
- "9330351d300f700f317ed2c72cbbb08522a827b962fe6ccb343e3ee71fdfd07c"
+ "5bea3e3ec6be3f1249ad19ed7b2d2e872c34025579a599f254de8e80cba9b3c7"
);
}
@@ -328,6 +357,10 @@ fn independent_validator_rejects_migration_or_schema_drift() {
SchemaVersionCatalog::computed_digest(8, [version_two_object()]).expect("v8 digest");
let version_eight = SchemaVersionCatalog::new(8, [version_two_object()], snapshot_digest)
.expect("version eight");
+ let snapshot_digest =
+ SchemaVersionCatalog::computed_digest(9, [version_two_object()]).expect("v9 digest");
+ let version_nine = SchemaVersionCatalog::new(9, [version_two_object()], snapshot_digest)
+ .expect("version nine");
let schema = SchemaCatalog::new(
&exact_migrations,
[
@@ -339,6 +372,7 @@ fn independent_validator_rejects_migration_or_schema_drift() {
version_six,
version_seven,
version_eight,
+ version_nine,
],
)
.expect("drift schema catalog");
@@ -401,6 +435,10 @@ fn catalog_errors_are_stable_source_free_and_redacted() {
SchemaVersionCatalog::computed_digest(8, [secret_object()]).expect("v8 digest");
let version_eight = SchemaVersionCatalog::new(8, [secret_object()], version_eight_digest)
.expect("version eight");
+ let version_nine_digest =
+ SchemaVersionCatalog::computed_digest(9, [secret_object()]).expect("v9 digest");
+ let version_nine =
+ SchemaVersionCatalog::new(9, [secret_object()], version_nine_digest).expect("version nine");
let schema = SchemaCatalog::new(
&migrations,
[
@@ -412,6 +450,7 @@ fn catalog_errors_are_stable_source_free_and_redacted() {
version_six,
version_seven,
version_eight,
+ version_nine,
],
)
.expect("schema catalog");
diff --git a/tests/services_hardening_state_host.rs b/tests/services_hardening_state_host.rs
@@ -106,13 +106,17 @@ async fn downgrade_fixture_to_schema_v7(runtime: &rhi::RhiRuntimeContext) {
.await
.expect("migration delete guard SQL");
for statement in [
+ "DROP TRIGGER presence_desired_state_guard_insert",
+ "DROP TRIGGER presence_desired_state_guard_update",
+ "DROP TRIGGER presence_desired_state_no_delete",
+ "DROP TABLE presence_desired_state",
"DROP TRIGGER reconciliation_jobs_shape_guard_insert",
"DROP TRIGGER reconciliation_jobs_shape_guard_update",
"DROP TRIGGER radroots_service_metadata_guard_update",
"DROP TRIGGER schema_migrations_no_update",
"DROP TRIGGER schema_migrations_no_delete",
"UPDATE radroots_service_metadata SET state_schema_version = 7 WHERE singleton = 1",
- "DELETE FROM schema_migrations WHERE version = 8",
+ "DELETE FROM schema_migrations WHERE version IN (8, 9)",
] {
sqlx::query(statement)
.execute(&mut connection)
@@ -418,7 +422,7 @@ async fn schema_v8_scans_historical_nullable_job_state_and_installs_permanent_gu
let (applied_at, build) = migration_evidence();
initialize_rhi_state(&runtime, &metadata, applied_at, &build)
.await
- .expect("schema-v8 initialization");
+ .expect("current-schema initialization");
downgrade_fixture_to_schema_v7(&runtime).await;
insert_historical_reconciliation_job(&runtime, state, next_attempt, owner, expiry).await;
@@ -450,7 +454,7 @@ async fn schema_v8_scans_historical_nullable_job_state_and_installs_permanent_gu
let (applied_at, build) = migration_evidence();
initialize_rhi_state(&runtime, &metadata, applied_at, &build)
.await
- .expect("schema-v8 initialization");
+ .expect("current-schema initialization");
downgrade_fixture_to_schema_v7(&runtime).await;
insert_historical_reconciliation_job(
&runtime,
@@ -479,7 +483,7 @@ async fn schema_v8_scans_historical_nullable_job_state_and_installs_permanent_gu
.fetch_one(&mut connection)
.await
.expect("migrated schema state");
- assert_eq!(migrated, (8, 1, 2, 0));
+ assert_eq!(migrated, (9, 1, 2, 0));
let invalid_insert = sqlx::query(
r#"INSERT INTO reconciliation_jobs (
diff --git a/tests/services_hardening_state_resilience.rs b/tests/services_hardening_state_resilience.rs
@@ -347,7 +347,7 @@ async fn exact_open_rejects_unexpected_migration_history_without_repair() {
service_version, service_commit, lib_revision, rust_version, target,
feature_profile, config_contract_version, state_contract_version,
admin_contract_version, status_contract_version, provider_contract_version
- ) VALUES (9, 'unexpected_schema', ?, 1725000000, '0.1.0', ?, ?,
+ ) VALUES (10, 'unexpected_schema', ?, 1725000000, '0.1.0', ?, ?,
'rustc-test', 'test-target', 'service-host', 1, 7, 1, 1, 1)",
)
.bind([0x44_u8; 32].as_slice())