commit 7e457dd7496b1ff5375acd60de51c58f8b569c2e
parent 662f79fd5a61302aa09481f95059f0e43bf24506
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 11:12:52 +0000
refactor(rhi): freeze publication authority
Diffstat:
16 files changed, 1886 insertions(+), 36 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -251,7 +251,9 @@
governed RHI schema-v2 configuration-binding migration, schema-v3
immutable trade-evidence migration, schema-v4 source-checkpoint and
dirty-generation migration, schema-v5 bounded reconciliation-job migration,
- and schema-v6 immutable reconciliation-attempt/source-result migration.
+ schema-v6 immutable reconciliation-attempt/source-result migration, and
+ schema-v7 immutable manifest, projection, report, exact signed-event, and
+ publication-workflow migration.
Retain at most 1,024 consecutive
immutable configuration generations containing only normalized
config/evidence-policy digests, public identity, exact contract versions,
@@ -268,6 +270,15 @@
use intent-open, discover source generation under retained authority, and
match the latest durable binding; configuration apply is an exclusive offline
operation.
+- Derive publication authority only from one complete validated configuration.
+ Required mode preserves the exact ordered write-relay target inventory,
+ requiredness, retry bounds, queue capacity, and domain-separated identities;
+ disabled mode has no target, retry, network, or hidden fallback authority.
+ Keep manifest, projection, report, signed-event bytes, and attempt rows
+ immutable, and expose outbox/target progress only through versioned
+ compare-and-swap state. Step 198 defines this catalog and performs no SQLite
+ mutation or relay I/O; later publication steps must use only the committed
+ exact signed bytes and may never rebuild, reserialize, or re-sign them.
- Commit one exact reconciliation-attempt replay inventory only through the
typed attempt repository. Revalidate the exact live lease, dirty generation,
evidence policy, and every scoped prior checkpoint before mutation; persist
diff --git a/Cargo.toml b/Cargo.toml
@@ -20,7 +20,7 @@ service = "rhi"
host_feature_profile = "service-host"
nix_material = "absent"
config_contract_version = 1
-state_contract_version = 6
+state_contract_version = 7
admin_contract_version = 1
status_contract_version = 1
provider_contract_version = 1
diff --git a/README b/README
@@ -316,11 +316,33 @@ The sealed result owns the finalization fence, canonical report, verified event
identifier, exact signed bytes, and the SHA-256 of those bytes. Later
persistence must retain those bytes without rebuilding, reserializing, or
re-signing them. This checkpoint performs no SQLite, filesystem, relay,
-network, task, ambient-clock, or ambient-entropy operation. Schema-v7 storage,
-the generation-fenced final transaction, publication, and job finalization
-remain with Steps 198 and 199. The exact machine contract is
+network, task, ambient-clock, or ambient-entropy operation. The schema-v7
+catalog is frozen by Step 198; the generation-fenced write, publication, and
+job finalization remain with Step 199 and its successors. The exact machine contract is
[`reconciliation_attestation.v1.json`](contracts/services_hardening/reconciliation_attestation.v1.json).
+## Explicit publication authority and durable schema
+
+`RhiPublicationAuthority::from_config` is the sole public derivation boundary
+for publication intent. It consumes one complete validated RHI configuration
+and returns exactly `Required` or `Disabled`. Required authority preserves the
+explicit ordered write-relay targets, each target's requiredness, the bounded
+retry policy, and queue capacity under separate domain-separated target-set
+and complete-authority digests. Disabled authority contains no target or retry
+state and authorizes no hidden network work. The sealed result exposes no URL,
+credential, transport, SQLite, clock, entropy, or task authority.
+
+Schema v7 freezes immutable manifest, projection, report, exact signed-event,
+and publication-attempt records plus compare-and-swap outbox and target rows.
+No-update/no-delete triggers protect semantic payload and target identities;
+the closed target evidence vocabulary distinguishes pending, submitted,
+accepted, rejected, rate-limited, auth-required, failed, and unknown. This
+checkpoint defines and verifies the catalog only. Step 199 owns the first
+atomic finalization write, and Steps 200-203 own claims, relay submission,
+outcomes, retry, recovery, and wave qualification. The exact machine contract
+is
+[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json).
+
## Existing-state runtime foundation
`open_rhi_runtime_foundation` opens only an already initialized database from
@@ -419,15 +441,17 @@ RHI owns one `state.sqlite` per service instance. Create-new initialization
starts from the shared schema-v1 baseline and immediately applies the pinned
schema-v2 configuration migration, schema-v3 immutable trade-evidence
migration, schema-v4 source-checkpoint and dirty-generation migration,
-schema-v5 bounded reconciliation-job migration, and schema-v6 immutable
-reconciliation-result migration.
-Version six contains the six shared immutable service-metadata and
+schema-v5 bounded reconciliation-job migration, schema-v6 immutable
+reconciliation-result migration, and schema-v7 immutable report, signed-event,
+and publication-workflow migration.
+Version seven contains the six shared immutable service-metadata and
migration-ledger objects, the bounded append-only `rhi_config_bindings` table,
separate immutable tables for canonical mutations, signed Nostr events, and
accepted source observations, and generation-guarded relay checkpoints and
per-trade dirty generations with their enforcement triggers and indexes.
-It also retains immutable attempt and source-result rows under no-update and
-no-delete triggers.
+It also retains immutable attempt, source-result, manifest, projection, report,
+signed-event, and publication-attempt rows plus compare-and-swap outbox and
+target state under exact transition and retention triggers.
Exact literal SHA-256 values bind both migrations, every schema snapshot, and
the schema catalog. RHI validates every identity before it can become database
authority.
diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt
@@ -137,6 +137,16 @@ pub enum rhi::RhiPublicationCommandV1
pub rhi::RhiPublicationCommandV1::Backlog
pub rhi::RhiPublicationCommandV1::Retry
pub rhi::RhiPublicationCommandV1::Targets
+pub enum rhi::RhiPublicationErrorKind
+pub rhi::RhiPublicationErrorKind::InvalidConfiguration
+pub rhi::RhiPublicationErrorKind::TargetInventory
+impl rhi::RhiPublicationErrorKind
+pub const fn rhi::RhiPublicationErrorKind::code(self) -> &'static str
+pub enum rhi::RhiPublicationMode
+pub rhi::RhiPublicationMode::Disabled
+pub rhi::RhiPublicationMode::Required
+impl rhi::RhiPublicationMode
+pub const fn rhi::RhiPublicationMode::code(self) -> &'static str
pub enum rhi::RhiReconciliationAttemptErrorKind
pub rhi::RhiReconciliationAttemptErrorKind::InvalidConfiguration
pub rhi::RhiReconciliationAttemptErrorKind::InvalidInput
@@ -639,12 +649,45 @@ pub const fn rhi::RhiPublicationAttemptRepository<'_>::descriptor(&self) -> rhi:
pub const fn rhi::RhiPublicationAttemptRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiPublicationAttemptRepository<'_>
pub fn rhi::RhiPublicationAttemptRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationAuthority
+impl rhi::RhiPublicationAuthority
+pub const fn rhi::RhiPublicationAuthority::authority_sha256(&self) -> &[u8; 32]
+pub fn rhi::RhiPublicationAuthority::from_config(&rhi::RhiConfigDocumentV1) -> core::result::Result<Self, rhi::RhiPublicationError>
+pub const fn rhi::RhiPublicationAuthority::mode(&self) -> rhi::RhiPublicationMode
+pub const fn rhi::RhiPublicationAuthority::queue_capacity(&self) -> u32
+pub const fn rhi::RhiPublicationAuthority::retry_policy(&self) -> core::option::Option<rhi::RhiPublicationRetryPolicy>
+pub const fn rhi::RhiPublicationAuthority::target_set_sha256(&self) -> &[u8; 32]
+pub fn rhi::RhiPublicationAuthority::targets(&self) -> &[rhi::RhiPublicationTarget]
+impl core::fmt::Debug for rhi::RhiPublicationAuthority
+pub fn rhi::RhiPublicationAuthority::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationError
+impl rhi::RhiPublicationError
+pub const fn rhi::RhiPublicationError::code(self) -> &'static str
+pub const fn rhi::RhiPublicationError::kind(self) -> rhi::RhiPublicationErrorKind
+impl core::error::Error for rhi::RhiPublicationError
+impl core::fmt::Debug for rhi::RhiPublicationError
+pub fn rhi::RhiPublicationError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiPublicationError
+pub fn rhi::RhiPublicationError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiPublicationOutboxRepository<'host>
impl rhi::RhiPublicationOutboxRepository<'_>
pub const fn rhi::RhiPublicationOutboxRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
pub const fn rhi::RhiPublicationOutboxRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiPublicationOutboxRepository<'_>
pub fn rhi::RhiPublicationOutboxRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationRetryPolicy
+impl rhi::RhiPublicationRetryPolicy
+pub const fn rhi::RhiPublicationRetryPolicy::attempt_deadline_milliseconds(self) -> u64
+pub const fn rhi::RhiPublicationRetryPolicy::initial_backoff_milliseconds(self) -> u64
+pub const fn rhi::RhiPublicationRetryPolicy::maximum_attempts(self) -> u16
+pub const fn rhi::RhiPublicationRetryPolicy::maximum_backoff_milliseconds(self) -> u64
+pub struct rhi::RhiPublicationTarget
+impl rhi::RhiPublicationTarget
+pub const fn rhi::RhiPublicationTarget::ordinal(&self) -> u8
+pub fn rhi::RhiPublicationTarget::relay_id(&self) -> &str
+pub const fn rhi::RhiPublicationTarget::required(&self) -> bool
+impl core::fmt::Debug for rhi::RhiPublicationTarget
+pub fn rhi::RhiPublicationTarget::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiPublicationTargetRepository<'host>
impl rhi::RhiPublicationTargetRepository<'_>
pub const fn rhi::RhiPublicationTargetRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
@@ -1336,6 +1379,9 @@ 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_PROVIDER_CONTRACT_VERSION: u32
+pub const rhi::RHI_PUBLICATION_CONTRACT_VERSION: u32
+pub const rhi::RHI_PUBLICATION_MAX_ATTEMPTS: u16
+pub const rhi::RHI_PUBLICATION_MAX_TARGETS: usize
pub const rhi::RHI_RECONCILIATION_ATTEMPT_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_ATTEMPT_MAX_SOURCES: usize
pub const rhi::RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION: u32
@@ -1375,6 +1421,9 @@ pub const rhi::RHI_STATE_SCHEMA_VERSION_5_SHA256: [u8; 32]
pub const rhi::RHI_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256: [u8; 32]
pub const rhi::RHI_STATE_SCHEMA_VERSION_6_OBJECT_COUNT: u32
pub const rhi::RHI_STATE_SCHEMA_VERSION_6_SHA256: [u8; 32]
+pub const rhi::RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256: [u8; 32]
+pub const rhi::RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT: u32
+pub const rhi::RHI_STATE_SCHEMA_VERSION_7_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
diff --git a/contracts/services_hardening/publication_outbox.v1.json b/contracts/services_hardening/publication_outbox.v1.json
@@ -0,0 +1,86 @@
+{
+ "schema": "radroots.rhi.publication-outbox",
+ "schema_version": 1,
+ "contract_version": 1,
+ "state_schema_version": 7,
+ "publication_modes": ["required", "disabled"],
+ "authority": {
+ "source": "complete_validated_rhi_config_v1",
+ "required": {
+ "target_count": { "minimum": 1, "maximum": 32 },
+ "target_order": "exact_configured_order",
+ "relay_authority": ["stable_id", "write_true", "required_boolean"],
+ "retry": ["maximum_attempts", "initial_backoff_ms", "maximum_backoff_ms", "attempt_deadline_ms"]
+ },
+ "disabled": {
+ "targets": "absent",
+ "retry": "absent",
+ "network_work": false
+ },
+ "target_set_digest": {
+ "algorithm": "sha256",
+ "domain": "radroots.rhi.publication_target_set.v1\\0",
+ "framing": ["target_count_u32_be", "ordinal_u32_be", "relay_id_length_u64_be", "relay_id_utf8", "required_u8"]
+ },
+ "complete_digest": {
+ "algorithm": "sha256",
+ "domain": "radroots.rhi.publication_authority.v1\\0",
+ "framing": ["mode_u8", "target_count_u32_be", "target_set_sha256", "retry_presence_u8", "retry_fields_be_when_present", "queue_capacity_u32_be"]
+ }
+ },
+ "schema_objects": {
+ "immutable": [
+ "evidence_manifests",
+ "trade_projections",
+ "attestation_reports",
+ "signed_attestation_events",
+ "publication_attempts"
+ ],
+ "compare_and_swap": ["publication_outbox", "publication_targets"],
+ "outbox_states": ["pending", "leased", "complete", "blocked"],
+ "target_states": ["pending", "submitted", "accepted", "rejected", "rate_limited", "auth_required", "failed", "unknown"],
+ "attempt_outcomes": ["submitted", "accepted", "rejected", "rate_limited", "auth_required", "failed", "unknown"],
+ "retention": "no_delete",
+ "accepted_target_is_terminal": true
+ },
+ "payload": {
+ "signed_bytes_maximum": 32768,
+ "stored_bytes": "exact_independently_verified_signed_event_json",
+ "stored_digest": "sha256_of_exact_signed_bytes",
+ "retry_source": "stored_bytes_only",
+ "target_set": "immutable_after_initial_commit"
+ },
+ "effects": {
+ "config_read": true,
+ "sqlite_catalog_definition": true,
+ "sqlite_query_or_mutation": false,
+ "relay_or_network": false,
+ "task_spawn": false,
+ "ambient_clock": false,
+ "ambient_entropy": false
+ },
+ "forbidden": [
+ "implicit_publication_mode",
+ "disabled_mode_target_or_retry_state",
+ "caller_forged_target",
+ "non_write_relay_target",
+ "mutable_signed_payload",
+ "mutable_target_identity",
+ "raw_upstream_error_text",
+ "raw_sqlite_handle",
+ "relay_io",
+ "event_rebuild",
+ "event_reserialize",
+ "event_resign",
+ "unbounded_queue_or_target_inventory"
+ ],
+ "deferred": [
+ "atomic_finalization_commit",
+ "publication_claim",
+ "relay_submission",
+ "target_outcome_transition",
+ "retry_and_recovery",
+ "runtime_worker",
+ "admin_routes"
+ ]
+}
diff --git a/radroots.service.source-lock.v2.toml b/radroots.service.source-lock.v2.toml
@@ -16,7 +16,7 @@ material = "absent"
[contract_versions]
config = 1
-state = 6
+state = 7
admin = 1
status = 1
provider = 1
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 publication;
mod reconciliation_attempt;
mod reconciliation_attestation;
mod reconciliation_commit;
@@ -64,6 +65,11 @@ pub use identity_envelope::{
RhiIdentityRole, RhiWrappingCredential, open_rhi_encrypted_identity,
provision_rhi_encrypted_identity,
};
+pub use publication::{
+ RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS,
+ RhiPublicationAuthority, RhiPublicationError, RhiPublicationErrorKind, RhiPublicationMode,
+ RhiPublicationRetryPolicy, RhiPublicationTarget,
+};
pub use radroots_event::id::TradeId;
pub use radroots_runtime_paths::{
INSTANCE_ID_MAX_BYTES, InstanceId, RadrootsHostEnvironment, RadrootsPathProfile,
@@ -153,8 +159,9 @@ pub use state_catalog::{
RHI_STATE_SCHEMA_VERSION_5_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_5_OBJECT_COUNT,
RHI_STATE_SCHEMA_VERSION_5_SHA256, RHI_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256,
RHI_STATE_SCHEMA_VERSION_6_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_6_SHA256,
- RhiStateCatalogError, RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
- validate_rhi_state_catalogs,
+ RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT,
+ RHI_STATE_SCHEMA_VERSION_7_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/publication.rs b/src/publication.rs
@@ -0,0 +1,533 @@
+//! Explicit publication authority and immutable target-set identity.
+
+use core::fmt;
+use std::error::Error;
+
+use serde_json::Value;
+use sha2::{Digest, Sha256};
+
+use crate::RhiConfigDocumentV1;
+
+/// Exact version of the RHI publication-authority contract.
+pub const RHI_PUBLICATION_CONTRACT_VERSION: u32 = 1;
+
+/// Absolute number of publication targets admitted by the v1 contract.
+pub const RHI_PUBLICATION_MAX_TARGETS: usize = 32;
+
+/// Absolute number of durable publication attempts admitted per target.
+pub const RHI_PUBLICATION_MAX_ATTEMPTS: u16 = 100;
+
+const AUTHORITY_DOMAIN: &[u8] = b"radroots.rhi.publication_authority.v1\0";
+const TARGET_SET_DOMAIN: &[u8] = b"radroots.rhi.publication_target_set.v1\0";
+
+/// Closed publication authority selected by the complete validated configuration.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RhiPublicationMode {
+ Required,
+ Disabled,
+}
+
+impl RhiPublicationMode {
+ /// Returns the exact machine-contract spelling.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::Required => "required",
+ Self::Disabled => "disabled",
+ }
+ }
+}
+
+/// Immutable retry authority copied from one validated required-publication config.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct RhiPublicationRetryPolicy {
+ maximum_attempts: u16,
+ initial_backoff_milliseconds: u64,
+ maximum_backoff_milliseconds: u64,
+ attempt_deadline_milliseconds: u64,
+}
+
+impl RhiPublicationRetryPolicy {
+ /// Returns the total allowed attempts for each target.
+ #[must_use]
+ pub const fn maximum_attempts(self) -> u16 {
+ self.maximum_attempts
+ }
+
+ /// Returns the configured initial retry bound in whole milliseconds.
+ #[must_use]
+ pub const fn initial_backoff_milliseconds(self) -> u64 {
+ self.initial_backoff_milliseconds
+ }
+
+ /// Returns the configured maximum retry bound in whole milliseconds.
+ #[must_use]
+ pub const fn maximum_backoff_milliseconds(self) -> u64 {
+ self.maximum_backoff_milliseconds
+ }
+
+ /// Returns the absolute per-attempt duration bound in whole milliseconds.
+ #[must_use]
+ pub const fn attempt_deadline_milliseconds(self) -> u64 {
+ self.attempt_deadline_milliseconds
+ }
+}
+
+/// One immutable publication target derived from the configured relay inventory.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct RhiPublicationTarget {
+ ordinal: u8,
+ relay_id: Box<str>,
+ required: bool,
+}
+
+impl RhiPublicationTarget {
+ /// Returns the stable zero-based position from the configured target inventory.
+ #[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 governed relay authority.
+ #[must_use]
+ pub const fn required(&self) -> bool {
+ self.required
+ }
+}
+
+impl fmt::Debug for RhiPublicationTarget {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPublicationTarget")
+ .field("ordinal", &self.ordinal)
+ .field("relay_id", &"[redacted]")
+ .field("required", &self.required)
+ .finish()
+ }
+}
+
+/// Sealed immutable publication authority derived from one validated config.
+///
+/// Disabled authority contains no target or retry state. Required authority
+/// preserves the complete configured target order and binds every target's
+/// requiredness, the retry policy, and queue bound into one domain-separated
+/// digest. Construction performs no I/O.
+///
+/// ```compile_fail
+/// use rhi::RhiPublicationAuthority;
+///
+/// let _forged = RhiPublicationAuthority { mode: todo!() };
+/// ```
+#[derive(Clone, PartialEq, Eq)]
+pub struct RhiPublicationAuthority {
+ mode: RhiPublicationMode,
+ targets: Box<[RhiPublicationTarget]>,
+ retry: Option<RhiPublicationRetryPolicy>,
+ queue_capacity: u32,
+ target_set_sha256: [u8; 32],
+ authority_sha256: [u8; 32],
+}
+
+impl RhiPublicationAuthority {
+ /// Derives the only publication authority from one complete admitted config.
+ pub fn from_config(config: &RhiConfigDocumentV1) -> Result<Self, RhiPublicationError> {
+ derive_authority(config.normalized())
+ }
+
+ /// Returns the explicit configured publication mode.
+ #[must_use]
+ pub const fn mode(&self) -> RhiPublicationMode {
+ self.mode
+ }
+
+ /// Returns the immutable configured target inventory.
+ #[must_use]
+ pub fn targets(&self) -> &[RhiPublicationTarget] {
+ &self.targets
+ }
+
+ /// Returns retry authority only when publication is required.
+ #[must_use]
+ pub const fn retry_policy(&self) -> Option<RhiPublicationRetryPolicy> {
+ self.retry
+ }
+
+ /// Returns the configured durable publication queue bound.
+ #[must_use]
+ pub const fn queue_capacity(&self) -> u32 {
+ self.queue_capacity
+ }
+
+ /// Returns the domain-separated immutable target-set identity.
+ #[must_use]
+ pub const fn target_set_sha256(&self) -> &[u8; 32] {
+ &self.target_set_sha256
+ }
+
+ /// Returns the domain-separated identity of the complete publication authority.
+ #[must_use]
+ pub const fn authority_sha256(&self) -> &[u8; 32] {
+ &self.authority_sha256
+ }
+}
+
+impl fmt::Debug for RhiPublicationAuthority {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPublicationAuthority")
+ .field("mode", &self.mode)
+ .field("target_count", &self.targets.len())
+ .field("queue_capacity", &self.queue_capacity)
+ .finish_non_exhaustive()
+ }
+}
+
+/// Stable source-free publication-authority construction failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiPublicationErrorKind {
+ InvalidConfiguration,
+ TargetInventory,
+}
+
+impl RhiPublicationErrorKind {
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidConfiguration => "publication_configuration_invalid",
+ Self::TargetInventory => "publication_target_inventory_invalid",
+ }
+ }
+}
+
+/// Redacted source-free publication-authority failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiPublicationError {
+ kind: RhiPublicationErrorKind,
+}
+
+impl RhiPublicationError {
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiPublicationErrorKind {
+ 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 RhiPublicationError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiPublicationErrorKind::InvalidConfiguration => {
+ "RHI publication configuration is invalid"
+ }
+ RhiPublicationErrorKind::TargetInventory => {
+ "RHI publication target inventory is invalid"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiPublicationError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPublicationError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiPublicationError {}
+
+fn derive_authority(document: &Value) -> Result<RhiPublicationAuthority, RhiPublicationError> {
+ let mode = match string(document, "/publication/mode")? {
+ "required" => RhiPublicationMode::Required,
+ "disabled" => RhiPublicationMode::Disabled,
+ _ => return Err(failure(RhiPublicationErrorKind::InvalidConfiguration)),
+ };
+ let queue_capacity =
+ integer(document, "/resource_limits/queues/publication").and_then(|value| {
+ u32::try_from(value).map_err(|_| failure(RhiPublicationErrorKind::InvalidConfiguration))
+ })?;
+ if queue_capacity == 0 || queue_capacity > 65_536 {
+ return Err(failure(RhiPublicationErrorKind::InvalidConfiguration));
+ }
+
+ let (targets, retry) = match mode {
+ RhiPublicationMode::Disabled => {
+ if document.pointer("/publication/target_relay_ids").is_some()
+ || document.pointer("/publication/retry").is_some()
+ {
+ return Err(failure(RhiPublicationErrorKind::InvalidConfiguration));
+ }
+ (Vec::new(), None)
+ }
+ RhiPublicationMode::Required => {
+ let target_ids = document
+ .pointer("/publication/target_relay_ids")
+ .and_then(Value::as_array)
+ .ok_or_else(|| failure(RhiPublicationErrorKind::TargetInventory))?;
+ if target_ids.is_empty() || target_ids.len() > RHI_PUBLICATION_MAX_TARGETS {
+ return Err(failure(RhiPublicationErrorKind::TargetInventory));
+ }
+ let relays = document
+ .pointer("/relays")
+ .and_then(Value::as_array)
+ .ok_or_else(|| failure(RhiPublicationErrorKind::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(RhiPublicationErrorKind::TargetInventory))?;
+ if targets
+ .iter()
+ .any(|target: &RhiPublicationTarget| target.relay_id() == relay_id)
+ {
+ return Err(failure(RhiPublicationErrorKind::TargetInventory));
+ }
+ let relay = relays
+ .iter()
+ .find(|relay| relay.pointer("/id").and_then(Value::as_str) == Some(relay_id))
+ .ok_or_else(|| failure(RhiPublicationErrorKind::TargetInventory))?;
+ if relay.pointer("/write").and_then(Value::as_bool) != Some(true) {
+ return Err(failure(RhiPublicationErrorKind::TargetInventory));
+ }
+ targets.push(RhiPublicationTarget {
+ ordinal: u8::try_from(ordinal)
+ .map_err(|_| failure(RhiPublicationErrorKind::TargetInventory))?,
+ relay_id: relay_id.into(),
+ required: relay
+ .pointer("/required")
+ .and_then(Value::as_bool)
+ .ok_or_else(|| failure(RhiPublicationErrorKind::TargetInventory))?,
+ });
+ }
+ let maximum_attempts =
+ integer(document, "/publication/retry/max_attempts").and_then(|value| {
+ u16::try_from(value)
+ .map_err(|_| failure(RhiPublicationErrorKind::InvalidConfiguration))
+ })?;
+ let retry = RhiPublicationRetryPolicy {
+ maximum_attempts,
+ initial_backoff_milliseconds: integer(
+ document,
+ "/publication/retry/initial_backoff_ms",
+ )?,
+ maximum_backoff_milliseconds: integer(
+ document,
+ "/publication/retry/maximum_backoff_ms",
+ )?,
+ attempt_deadline_milliseconds: integer(
+ document,
+ "/publication/retry/attempt_deadline_ms",
+ )?,
+ };
+ if retry.maximum_attempts == 0
+ || retry.maximum_attempts > RHI_PUBLICATION_MAX_ATTEMPTS
+ || retry.initial_backoff_milliseconds == 0
+ || retry.initial_backoff_milliseconds > retry.maximum_backoff_milliseconds
+ || retry.maximum_backoff_milliseconds > 3_600_000
+ || !(100..=30_000).contains(&retry.attempt_deadline_milliseconds)
+ {
+ return Err(failure(RhiPublicationErrorKind::InvalidConfiguration));
+ }
+ (targets, Some(retry))
+ }
+ };
+
+ let target_set_sha256 = target_set_digest(&targets)?;
+ let authority_sha256 = authority_digest(
+ mode,
+ &target_set_sha256,
+ targets.len(),
+ retry,
+ queue_capacity,
+ )?;
+ Ok(RhiPublicationAuthority {
+ mode,
+ targets: targets.into_boxed_slice(),
+ retry,
+ queue_capacity,
+ target_set_sha256,
+ authority_sha256,
+ })
+}
+
+fn target_set_digest(targets: &[RhiPublicationTarget]) -> Result<[u8; 32], RhiPublicationError> {
+ let mut digest = Sha256::new();
+ digest.update(TARGET_SET_DOMAIN);
+ digest.update(
+ u32::try_from(targets.len())
+ .map_err(|_| failure(RhiPublicationErrorKind::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(RhiPublicationErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ digest.update(target.relay_id.as_bytes());
+ digest.update([u8::from(target.required)]);
+ }
+ Ok(digest.finalize().into())
+}
+
+fn authority_digest(
+ mode: RhiPublicationMode,
+ target_set_sha256: &[u8; 32],
+ target_count: usize,
+ retry: Option<RhiPublicationRetryPolicy>,
+ queue_capacity: u32,
+) -> Result<[u8; 32], RhiPublicationError> {
+ let mut digest = Sha256::new();
+ digest.update(AUTHORITY_DOMAIN);
+ digest.update([match mode {
+ RhiPublicationMode::Disabled => 0,
+ RhiPublicationMode::Required => 1,
+ }]);
+ digest.update(
+ u32::try_from(target_count)
+ .map_err(|_| failure(RhiPublicationErrorKind::TargetInventory))?
+ .to_be_bytes(),
+ );
+ digest.update(target_set_sha256);
+ match retry {
+ Some(retry) => {
+ digest.update([1]);
+ digest.update(retry.maximum_attempts.to_be_bytes());
+ digest.update(retry.initial_backoff_milliseconds.to_be_bytes());
+ digest.update(retry.maximum_backoff_milliseconds.to_be_bytes());
+ digest.update(retry.attempt_deadline_milliseconds.to_be_bytes());
+ }
+ None => digest.update([0]),
+ }
+ digest.update(queue_capacity.to_be_bytes());
+ Ok(digest.finalize().into())
+}
+
+fn string<'a>(document: &'a Value, pointer: &str) -> Result<&'a str, RhiPublicationError> {
+ document
+ .pointer(pointer)
+ .and_then(Value::as_str)
+ .ok_or_else(|| failure(RhiPublicationErrorKind::InvalidConfiguration))
+}
+
+fn integer(document: &Value, pointer: &str) -> Result<u64, RhiPublicationError> {
+ document
+ .pointer(pointer)
+ .and_then(Value::as_u64)
+ .ok_or_else(|| failure(RhiPublicationErrorKind::InvalidConfiguration))
+}
+
+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'-')
+ })
+}
+
+const fn failure(kind: RhiPublicationErrorKind) -> RhiPublicationError {
+ RhiPublicationError { kind }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{RhiConfigProfile, parse_rhi_config_v1};
+
+ const EXAMPLE: &[u8] = include_bytes!("../contracts/services_hardening/config.v1.example.toml");
+
+ #[test]
+ fn required_and_disabled_authority_are_exact_and_deterministic() {
+ let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
+ let first = RhiPublicationAuthority::from_config(&config).expect("authority");
+ let second = RhiPublicationAuthority::from_config(&config).expect("authority");
+ assert_eq!(first, second);
+ assert_eq!(first.mode(), RhiPublicationMode::Required);
+ assert_eq!(first.queue_capacity(), 4_096);
+ 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());
+ let retry = first.retry_policy().expect("required retry");
+ assert_eq!(retry.maximum_attempts(), 10);
+ assert_eq!(retry.initial_backoff_milliseconds(), 250);
+ assert_eq!(retry.maximum_backoff_milliseconds(), 30_000);
+ assert_eq!(retry.attempt_deadline_milliseconds(), 15_000);
+ assert_ne!(first.target_set_sha256(), &[0; 32]);
+ assert_ne!(first.authority_sha256(), &[0; 32]);
+
+ let source = core::str::from_utf8(EXAMPLE).expect("utf8");
+ let publication = source.find("[publication]").expect("publication section");
+ let presence = source.find("[presence]").expect("presence section");
+ let disabled = format!(
+ "{}[publication]\nmode = \"disabled\"\n\n{}",
+ &source[..publication],
+ &source[presence..]
+ );
+ let config = parse_rhi_config_v1(disabled.as_bytes(), RhiConfigProfile::Production)
+ .expect("disabled config");
+ let disabled = RhiPublicationAuthority::from_config(&config).expect("disabled authority");
+ assert_eq!(disabled.mode(), RhiPublicationMode::Disabled);
+ assert!(disabled.targets().is_empty());
+ assert_eq!(disabled.retry_policy(), None);
+ assert_eq!(disabled.queue_capacity(), 4_096);
+ assert_ne!(disabled.authority_sha256(), first.authority_sha256());
+ }
+
+ #[test]
+ fn target_order_requiredness_and_retry_change_the_authority_digest() {
+ let source = core::str::from_utf8(EXAMPLE).expect("utf8");
+ let baseline = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
+ let baseline = RhiPublicationAuthority::from_config(&baseline).expect("authority");
+ for changed in [
+ source.replace(
+ "target_relay_ids = [\"relay-primary\", \"relay-secondary\"]",
+ "target_relay_ids = [\"relay-secondary\", \"relay-primary\"]",
+ ),
+ source.replacen("required = true", "required = false", 1),
+ source.replace("max_attempts = 10", "max_attempts = 9"),
+ source.replace("publication = 4096", "publication = 4095"),
+ ] {
+ let config = parse_rhi_config_v1(changed.as_bytes(), RhiConfigProfile::Production)
+ .expect("changed config");
+ let changed = RhiPublicationAuthority::from_config(&config).expect("authority");
+ assert_ne!(changed.authority_sha256(), baseline.authority_sha256());
+ }
+ }
+
+ #[test]
+ fn public_diagnostics_are_source_free_and_redacted() {
+ for kind in [
+ RhiPublicationErrorKind::InvalidConfiguration,
+ RhiPublicationErrorKind::TargetInventory,
+ ] {
+ let error = failure(kind);
+ assert_eq!(error.kind(), kind);
+ assert!(error.code().starts_with("publication_"));
+ assert!(Error::source(&error).is_none());
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains("relay-secret"));
+ assert!(!rendered.contains("wss://"));
+ }
+ }
+}
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 = 6;
+pub const RHI_STATE_SCHEMA_VERSION: u32 = 7;
/// The shared metadata and migration-ledger objects present at schema v1.
pub const RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT: u32 = 6;
@@ -32,10 +32,13 @@ pub const RHI_STATE_SCHEMA_VERSION_5_OBJECT_COUNT: u32 = 33;
/// The shared objects plus immutable reconciliation attempt/source results.
pub const RHI_STATE_SCHEMA_VERSION_6_OBJECT_COUNT: u32 = 39;
+/// The shared objects plus immutable report and publication workflow state.
+pub const RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT: u32 = 63;
+
/// SHA-256 identity of the ordered migration catalog rooted at schema v1.
pub const RHI_MIGRATION_CATALOG_SHA256: [u8; 32] = [
- 0x32, 0xf4, 0x9f, 0x1c, 0x50, 0xf5, 0x4c, 0x72, 0x2d, 0x94, 0xd9, 0x34, 0x3a, 0x05, 0x2e, 0x67,
- 0x14, 0x2d, 0x00, 0x74, 0x7a, 0x0b, 0xb1, 0xcc, 0x1c, 0xb5, 0xca, 0xba, 0xfa, 0x30, 0xfe, 0x4c,
+ 0xbf, 0x95, 0x58, 0x84, 0xe9, 0x73, 0xde, 0x04, 0xb9, 0x8a, 0x57, 0x98, 0x65, 0x62, 0xef, 0x38,
+ 0x05, 0xad, 0x82, 0xce, 0x3d, 0xa6, 0xa8, 0x3a, 0xb5, 0x89, 0x0a, 0x50, 0xe0, 0x6b, 0xd5, 0xf4,
];
/// SHA-256 identity of the exact schema-v1 object snapshot.
@@ -104,10 +107,22 @@ pub const RHI_STATE_SCHEMA_VERSION_6_SHA256: [u8; 32] = [
0xd5, 0x1f, 0x92, 0x94, 0xc7, 0x52, 0x72, 0x04, 0x1f, 0x34, 0x9e, 0x95, 0xce, 0xd5, 0x0f, 0xb4,
];
+/// SHA-256 identity of the schema-v7 report/publication migration.
+pub const RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256: [u8; 32] = [
+ 0x9d, 0xeb, 0xf4, 0xf3, 0xca, 0xad, 0x4d, 0x01, 0x83, 0x11, 0xcb, 0x16, 0x9b, 0xf9, 0x28, 0x22,
+ 0x64, 0xd6, 0xab, 0xfc, 0x49, 0xe7, 0x18, 0x84, 0x0b, 0x9c, 0xbf, 0xad, 0x42, 0xed, 0x35, 0x7c,
+];
+
+/// SHA-256 identity of the exact schema-v7 object snapshot.
+pub const RHI_STATE_SCHEMA_VERSION_7_SHA256: [u8; 32] = [
+ 0x84, 0x0a, 0xa8, 0x3c, 0x68, 0x9f, 0x9d, 0xf9, 0x9d, 0x26, 0xc5, 0xb4, 0xef, 0x11, 0x71, 0x31,
+ 0xc2, 0x70, 0xef, 0x75, 0x22, 0x67, 0x40, 0x37, 0xde, 0xf7, 0x96, 0x28, 0xa2, 0xb0, 0x39, 0x46,
+];
+
/// SHA-256 identity of the schema catalog bound to the migration catalog.
pub const RHI_STATE_SCHEMA_CATALOG_SHA256: [u8; 32] = [
- 0x8c, 0x19, 0x82, 0xb2, 0x95, 0xd6, 0x54, 0x4b, 0xbe, 0x6d, 0xf6, 0x79, 0x42, 0x4e, 0xe6, 0xca,
- 0xf3, 0x47, 0x68, 0x31, 0xb3, 0x8d, 0xe9, 0x94, 0x6b, 0x11, 0x5f, 0xbc, 0x5b, 0x24, 0x6b, 0x03,
+ 0xec, 0x31, 0x80, 0x9d, 0x62, 0x07, 0xfd, 0x98, 0xb2, 0x04, 0xe5, 0x69, 0x39, 0x73, 0x11, 0x97,
+ 0xf2, 0x97, 0x87, 0xbd, 0x1f, 0xef, 0x62, 0x8d, 0x9b, 0x34, 0x5d, 0xb0, 0x20, 0x71, 0x1f, 0xec,
];
macro_rules! rhi_config_bindings_table_sql {
@@ -748,6 +763,429 @@ const CREATE_RECONCILIATION_RESULTS_MIGRATION_SQL: &str = concat!(
";",
);
+macro_rules! evidence_manifests_table_sql {
+ () => {
+ r#"CREATE TABLE evidence_manifests (
+ manifest_sha256 BLOB NOT NULL PRIMARY KEY CHECK (length(manifest_sha256) = 32),
+ attempt_id BLOB NOT NULL UNIQUE CHECK (length(attempt_id) = 32),
+ trade_id BLOB NOT NULL CHECK (length(trade_id) = 16),
+ trade_generation INTEGER NOT NULL
+ CHECK (trade_generation BETWEEN 1 AND 9223372036854775807),
+ evidence_policy_sha256 BLOB NOT NULL CHECK (length(evidence_policy_sha256) = 32),
+ canonical_manifest BLOB NOT NULL
+ CHECK (length(canonical_manifest) BETWEEN 1 AND 16777216),
+ observed_at_unix_s INTEGER NOT NULL
+ CHECK (observed_at_unix_s BETWEEN 0 AND 9223372036854775807),
+ source_count INTEGER NOT NULL CHECK (source_count BETWEEN 1 AND 16),
+ observation_count INTEGER NOT NULL CHECK (observation_count BETWEEN 0 AND 65536),
+ FOREIGN KEY (attempt_id) REFERENCES evidence_reconciliations (attempt_id)
+) STRICT"#
+ };
+}
+
+macro_rules! trade_projections_table_sql {
+ () => {
+ r#"CREATE TABLE trade_projections (
+ projection_sha256 BLOB NOT NULL PRIMARY KEY CHECK (length(projection_sha256) = 32),
+ manifest_sha256 BLOB NOT NULL UNIQUE CHECK (length(manifest_sha256) = 32),
+ shared_projection_sha256 BLOB NOT NULL CHECK (length(shared_projection_sha256) = 32),
+ reducer_contract TEXT NOT NULL CHECK (reducer_contract = 'radroots.trade.reducer.v1'),
+ reducer_contract_version INTEGER NOT NULL CHECK (reducer_contract_version = 1),
+ issue_count INTEGER NOT NULL CHECK (issue_count BETWEEN 0 AND 65536),
+ FOREIGN KEY (manifest_sha256) REFERENCES evidence_manifests (manifest_sha256)
+) STRICT"#
+ };
+}
+
+macro_rules! attestation_reports_table_sql {
+ () => {
+ r#"CREATE TABLE attestation_reports (
+ statement_sha256 BLOB NOT NULL PRIMARY KEY CHECK (length(statement_sha256) = 32),
+ manifest_sha256 BLOB NOT NULL CHECK (length(manifest_sha256) = 32),
+ projection_sha256 BLOB NOT NULL CHECK (length(projection_sha256) = 32),
+ trade_id BLOB NOT NULL CHECK (length(trade_id) = 16),
+ claim_mutation_id BLOB NOT NULL CHECK (length(claim_mutation_id) = 32),
+ issuer_public_key BLOB NOT NULL CHECK (length(issuer_public_key) = 32),
+ outcome TEXT NOT NULL CHECK (outcome IN ('valid', 'invalid', 'indeterminate')),
+ canonical_report BLOB NOT NULL CHECK (length(canonical_report) BETWEEN 1 AND 16384),
+ observed_at_unix_s INTEGER NOT NULL
+ CHECK (observed_at_unix_s BETWEEN 0 AND 9223372036854775807),
+ supersedes_statement_sha256 BLOB CHECK (length(supersedes_statement_sha256) = 32),
+ supersedes_event_id BLOB CHECK (length(supersedes_event_id) = 32),
+ FOREIGN KEY (manifest_sha256) REFERENCES evidence_manifests (manifest_sha256),
+ FOREIGN KEY (projection_sha256) REFERENCES trade_projections (projection_sha256),
+ FOREIGN KEY (supersedes_statement_sha256)
+ REFERENCES attestation_reports (statement_sha256),
+ CHECK ((supersedes_statement_sha256 IS NULL) = (supersedes_event_id IS NULL))
+) STRICT"#
+ };
+}
+
+macro_rules! attestation_reports_supersession_sql {
+ () => {
+ r#"CREATE UNIQUE INDEX attestation_reports_one_successor
+ON attestation_reports (supersedes_statement_sha256)
+WHERE supersedes_statement_sha256 IS NOT NULL"#
+ };
+}
+
+macro_rules! signed_attestation_events_table_sql {
+ () => {
+ r#"CREATE TABLE signed_attestation_events (
+ event_id BLOB NOT NULL PRIMARY KEY CHECK (length(event_id) = 32),
+ statement_sha256 BLOB NOT NULL UNIQUE CHECK (length(statement_sha256) = 32),
+ event_sha256 BLOB NOT NULL UNIQUE CHECK (length(event_sha256) = 32),
+ issuer_public_key BLOB NOT NULL CHECK (length(issuer_public_key) = 32),
+ authored_at_unix_s INTEGER NOT NULL
+ CHECK (authored_at_unix_s BETWEEN 0 AND 9223372036854775807),
+ canonical_event_json BLOB NOT NULL
+ CHECK (length(canonical_event_json) BETWEEN 1 AND 32768),
+ FOREIGN KEY (statement_sha256) REFERENCES attestation_reports (statement_sha256)
+) STRICT"#
+ };
+}
+
+macro_rules! publication_outbox_table_sql {
+ () => {
+ r#"CREATE TABLE publication_outbox (
+ outbox_id BLOB NOT NULL PRIMARY KEY CHECK (length(outbox_id) = 32),
+ event_id BLOB NOT NULL UNIQUE CHECK (length(event_id) = 32),
+ event_sha256 BLOB NOT NULL CHECK (length(event_sha256) = 32),
+ publication_authority_sha256 BLOB NOT NULL
+ CHECK (length(publication_authority_sha256) = 32),
+ target_set_sha256 BLOB NOT NULL CHECK (length(target_set_sha256) = 32),
+ target_count INTEGER NOT NULL CHECK (target_count BETWEEN 1 AND 32),
+ required_target_count INTEGER NOT NULL
+ CHECK (required_target_count BETWEEN 0 AND target_count),
+ max_attempts INTEGER NOT NULL CHECK (max_attempts BETWEEN 1 AND 100),
+ initial_backoff_ms INTEGER NOT NULL CHECK (initial_backoff_ms BETWEEN 1 AND 60000),
+ maximum_backoff_ms INTEGER NOT NULL CHECK (maximum_backoff_ms BETWEEN 1 AND 3600000),
+ attempt_deadline_ms INTEGER NOT NULL CHECK (attempt_deadline_ms BETWEEN 100 AND 30000),
+ state TEXT NOT NULL CHECK (state IN ('pending', 'leased', 'complete', 'blocked')),
+ revision INTEGER NOT NULL CHECK (revision BETWEEN 1 AND 9223372036854775807),
+ next_attempt_unix_ms INTEGER,
+ lease_owner BLOB,
+ lease_expires_unix_ms INTEGER,
+ created_at_unix_ms INTEGER NOT NULL
+ CHECK (created_at_unix_ms BETWEEN 0 AND 9223372036854775807),
+ updated_at_unix_ms INTEGER NOT NULL
+ CHECK (updated_at_unix_ms BETWEEN created_at_unix_ms AND 9223372036854775807),
+ FOREIGN KEY (event_id) REFERENCES signed_attestation_events (event_id),
+ CHECK (initial_backoff_ms <= maximum_backoff_ms),
+ CHECK (
+ (state = 'pending'
+ AND next_attempt_unix_ms IS NOT NULL
+ AND next_attempt_unix_ms BETWEEN 0 AND 9223372036854775807
+ AND lease_owner IS NULL AND lease_expires_unix_ms IS NULL)
+ OR (state = 'leased'
+ AND next_attempt_unix_ms IS NULL
+ AND lease_owner IS NOT NULL
+ AND length(lease_owner) = 16
+ AND lease_expires_unix_ms IS NOT NULL
+ AND lease_expires_unix_ms BETWEEN 1 AND 9223372036854775807)
+ OR (state IN ('complete', 'blocked')
+ AND next_attempt_unix_ms IS NULL
+ AND lease_owner IS NULL AND lease_expires_unix_ms IS NULL)
+ )
+) STRICT"#
+ };
+}
+
+macro_rules! publication_outbox_schedule_sql {
+ () => {
+ r#"CREATE INDEX publication_outbox_by_schedule
+ON publication_outbox (
+ state, next_attempt_unix_ms, lease_expires_unix_ms,
+ created_at_unix_ms, outbox_id
+)"#
+ };
+}
+
+macro_rules! publication_outbox_guard_update_sql {
+ () => {
+ r#"CREATE TRIGGER publication_outbox_guard_update
+BEFORE UPDATE ON publication_outbox
+WHEN NEW.outbox_id != OLD.outbox_id
+ OR NEW.event_id != OLD.event_id
+ OR NEW.event_sha256 != OLD.event_sha256
+ OR NEW.publication_authority_sha256 != OLD.publication_authority_sha256
+ OR NEW.target_set_sha256 != OLD.target_set_sha256
+ OR NEW.target_count != OLD.target_count
+ OR NEW.required_target_count != OLD.required_target_count
+ OR NEW.max_attempts != OLD.max_attempts
+ OR NEW.initial_backoff_ms != OLD.initial_backoff_ms
+ OR NEW.maximum_backoff_ms != OLD.maximum_backoff_ms
+ OR NEW.attempt_deadline_ms != OLD.attempt_deadline_ms
+ OR NEW.created_at_unix_ms != OLD.created_at_unix_ms
+ OR NEW.revision != OLD.revision + 1
+ OR NEW.updated_at_unix_ms < OLD.updated_at_unix_ms
+ OR NOT (
+ (OLD.state = 'pending' AND NEW.state IN ('leased', 'blocked'))
+ OR (OLD.state = 'leased'
+ AND NEW.state IN ('leased', 'pending', 'complete', 'blocked'))
+ OR (OLD.state = 'blocked' AND NEW.state = 'pending')
+ )
+BEGIN
+ SELECT RAISE(ABORT, 'publication outbox transition is invalid');
+END"#
+ };
+}
+
+macro_rules! publication_targets_table_sql {
+ () => {
+ r#"CREATE TABLE publication_targets (
+ outbox_id BLOB NOT NULL CHECK (length(outbox_id) = 32),
+ target_ordinal INTEGER NOT NULL CHECK (target_ordinal BETWEEN 0 AND 31),
+ relay_id TEXT NOT NULL
+ CHECK (length(CAST(relay_id AS BLOB)) BETWEEN 1 AND 64)
+ CHECK (relay_id NOT GLOB '*[^a-z0-9_-]*')
+ CHECK (substr(relay_id, 1, 1) GLOB '[a-z]'),
+ required INTEGER NOT NULL CHECK (required IN (0, 1)),
+ state TEXT NOT NULL CHECK (state IN (
+ 'pending', 'submitted', 'accepted', 'rejected', 'rate_limited',
+ 'auth_required', 'failed', 'unknown'
+ )),
+ revision INTEGER NOT NULL CHECK (revision BETWEEN 1 AND 9223372036854775807),
+ attempt_count INTEGER NOT NULL CHECK (attempt_count BETWEEN 0 AND 100),
+ next_attempt_unix_ms INTEGER
+ CHECK (next_attempt_unix_ms BETWEEN 0 AND 9223372036854775807),
+ last_attempt_id BLOB CHECK (length(last_attempt_id) = 32),
+ updated_at_unix_ms INTEGER NOT NULL
+ CHECK (updated_at_unix_ms BETWEEN 0 AND 9223372036854775807),
+ PRIMARY KEY (outbox_id, target_ordinal),
+ UNIQUE (outbox_id, relay_id),
+ FOREIGN KEY (outbox_id) REFERENCES publication_outbox (outbox_id),
+ CHECK ((attempt_count = 0) = (last_attempt_id IS NULL))
+) STRICT"#
+ };
+}
+
+macro_rules! publication_targets_schedule_sql {
+ () => {
+ r#"CREATE INDEX publication_targets_by_schedule
+ON publication_targets (state, next_attempt_unix_ms, updated_at_unix_ms, outbox_id, target_ordinal)"#
+ };
+}
+
+macro_rules! publication_targets_guard_update_sql {
+ () => {
+ r#"CREATE TRIGGER publication_targets_guard_update
+BEFORE UPDATE ON publication_targets
+WHEN NEW.outbox_id != OLD.outbox_id
+ OR NEW.target_ordinal != OLD.target_ordinal
+ OR NEW.relay_id != OLD.relay_id
+ OR NEW.required != OLD.required
+ OR NEW.revision != OLD.revision + 1
+ OR NEW.attempt_count < OLD.attempt_count
+ OR NEW.attempt_count > OLD.attempt_count + 1
+ OR NEW.updated_at_unix_ms < OLD.updated_at_unix_ms
+ OR OLD.state = 'accepted'
+BEGIN
+ SELECT RAISE(ABORT, 'publication target transition is invalid');
+END"#
+ };
+}
+
+macro_rules! publication_attempts_table_sql {
+ () => {
+ r#"CREATE TABLE publication_attempts (
+ attempt_id BLOB NOT NULL PRIMARY KEY CHECK (length(attempt_id) = 32),
+ outbox_id BLOB NOT NULL CHECK (length(outbox_id) = 32),
+ target_ordinal INTEGER NOT NULL CHECK (target_ordinal BETWEEN 0 AND 31),
+ attempt_number INTEGER NOT NULL CHECK (attempt_number BETWEEN 1 AND 100),
+ event_sha256 BLOB NOT NULL CHECK (length(event_sha256) = 32),
+ lease_owner BLOB NOT NULL CHECK (length(lease_owner) = 16),
+ started_at_unix_ms INTEGER NOT NULL
+ CHECK (started_at_unix_ms BETWEEN 0 AND 9223372036854775807),
+ finished_at_unix_ms INTEGER NOT NULL
+ CHECK (finished_at_unix_ms BETWEEN started_at_unix_ms AND 9223372036854775807),
+ outcome TEXT NOT NULL CHECK (outcome IN (
+ 'submitted', 'accepted', 'rejected', 'rate_limited',
+ 'auth_required', 'failed', 'unknown'
+ )),
+ result_code TEXT NOT NULL
+ CHECK (length(CAST(result_code AS BLOB)) BETWEEN 1 AND 64)
+ CHECK (result_code NOT GLOB '*[^a-z0-9_]*')
+ CHECK (substr(result_code, 1, 1) GLOB '[a-z]'),
+ UNIQUE (outbox_id, target_ordinal, attempt_number),
+ FOREIGN KEY (outbox_id, target_ordinal)
+ REFERENCES publication_targets (outbox_id, target_ordinal)
+) STRICT"#
+ };
+}
+
+const CREATE_EVIDENCE_MANIFESTS_TABLE_SQL: &str = evidence_manifests_table_sql!();
+const CREATE_EVIDENCE_MANIFESTS_NO_UPDATE_SQL: &str = immutable_no_update_sql!(
+ "evidence_manifests_no_update",
+ "evidence_manifests",
+ "evidence manifests are immutable"
+);
+const CREATE_EVIDENCE_MANIFESTS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "evidence_manifests_no_delete",
+ "evidence_manifests",
+ "evidence manifests are retained"
+);
+const CREATE_TRADE_PROJECTIONS_TABLE_SQL: &str = trade_projections_table_sql!();
+const CREATE_TRADE_PROJECTIONS_NO_UPDATE_SQL: &str = immutable_no_update_sql!(
+ "trade_projections_no_update",
+ "trade_projections",
+ "trade projections are immutable"
+);
+const CREATE_TRADE_PROJECTIONS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "trade_projections_no_delete",
+ "trade_projections",
+ "trade projections are retained"
+);
+const CREATE_ATTESTATION_REPORTS_TABLE_SQL: &str = attestation_reports_table_sql!();
+const CREATE_ATTESTATION_REPORTS_SUPERSESSION_SQL: &str = attestation_reports_supersession_sql!();
+const CREATE_ATTESTATION_REPORTS_NO_UPDATE_SQL: &str = immutable_no_update_sql!(
+ "attestation_reports_no_update",
+ "attestation_reports",
+ "attestation reports are immutable"
+);
+const CREATE_ATTESTATION_REPORTS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "attestation_reports_no_delete",
+ "attestation_reports",
+ "attestation reports are retained"
+);
+const CREATE_SIGNED_ATTESTATION_EVENTS_TABLE_SQL: &str = signed_attestation_events_table_sql!();
+const CREATE_SIGNED_ATTESTATION_EVENTS_NO_UPDATE_SQL: &str = immutable_no_update_sql!(
+ "signed_attestation_events_no_update",
+ "signed_attestation_events",
+ "signed attestation events are immutable"
+);
+const CREATE_SIGNED_ATTESTATION_EVENTS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "signed_attestation_events_no_delete",
+ "signed_attestation_events",
+ "signed attestation events are retained"
+);
+const CREATE_PUBLICATION_OUTBOX_TABLE_SQL: &str = publication_outbox_table_sql!();
+const CREATE_PUBLICATION_OUTBOX_SCHEDULE_SQL: &str = publication_outbox_schedule_sql!();
+const CREATE_PUBLICATION_OUTBOX_GUARD_UPDATE_SQL: &str = publication_outbox_guard_update_sql!();
+const CREATE_PUBLICATION_OUTBOX_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "publication_outbox_no_delete",
+ "publication_outbox",
+ "publication outbox rows are retained"
+);
+const CREATE_PUBLICATION_TARGETS_TABLE_SQL: &str = publication_targets_table_sql!();
+const CREATE_PUBLICATION_TARGETS_SCHEDULE_SQL: &str = publication_targets_schedule_sql!();
+const CREATE_PUBLICATION_TARGETS_GUARD_UPDATE_SQL: &str = publication_targets_guard_update_sql!();
+const CREATE_PUBLICATION_TARGETS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "publication_targets_no_delete",
+ "publication_targets",
+ "publication targets are retained"
+);
+const CREATE_PUBLICATION_ATTEMPTS_TABLE_SQL: &str = publication_attempts_table_sql!();
+const CREATE_PUBLICATION_ATTEMPTS_NO_UPDATE_SQL: &str = immutable_no_update_sql!(
+ "publication_attempts_no_update",
+ "publication_attempts",
+ "publication attempts are immutable"
+);
+const CREATE_PUBLICATION_ATTEMPTS_NO_DELETE_SQL: &str = immutable_no_delete_sql!(
+ "publication_attempts_no_delete",
+ "publication_attempts",
+ "publication attempts are retained"
+);
+
+const CREATE_REPORT_PUBLICATION_MIGRATION_SQL: &str = concat!(
+ evidence_manifests_table_sql!(),
+ ";\n",
+ immutable_no_update_sql!(
+ "evidence_manifests_no_update",
+ "evidence_manifests",
+ "evidence manifests are immutable"
+ ),
+ ";\n",
+ immutable_no_delete_sql!(
+ "evidence_manifests_no_delete",
+ "evidence_manifests",
+ "evidence manifests are retained"
+ ),
+ ";\n",
+ trade_projections_table_sql!(),
+ ";\n",
+ immutable_no_update_sql!(
+ "trade_projections_no_update",
+ "trade_projections",
+ "trade projections are immutable"
+ ),
+ ";\n",
+ immutable_no_delete_sql!(
+ "trade_projections_no_delete",
+ "trade_projections",
+ "trade projections are retained"
+ ),
+ ";\n",
+ attestation_reports_table_sql!(),
+ ";\n",
+ attestation_reports_supersession_sql!(),
+ ";\n",
+ immutable_no_update_sql!(
+ "attestation_reports_no_update",
+ "attestation_reports",
+ "attestation reports are immutable"
+ ),
+ ";\n",
+ immutable_no_delete_sql!(
+ "attestation_reports_no_delete",
+ "attestation_reports",
+ "attestation reports are retained"
+ ),
+ ";\n",
+ signed_attestation_events_table_sql!(),
+ ";\n",
+ immutable_no_update_sql!(
+ "signed_attestation_events_no_update",
+ "signed_attestation_events",
+ "signed attestation events are immutable"
+ ),
+ ";\n",
+ immutable_no_delete_sql!(
+ "signed_attestation_events_no_delete",
+ "signed_attestation_events",
+ "signed attestation events are retained"
+ ),
+ ";\n",
+ publication_outbox_table_sql!(),
+ ";\n",
+ publication_outbox_schedule_sql!(),
+ ";\n",
+ publication_outbox_guard_update_sql!(),
+ ";\n",
+ immutable_no_delete_sql!(
+ "publication_outbox_no_delete",
+ "publication_outbox",
+ "publication outbox rows are retained"
+ ),
+ ";\n",
+ publication_targets_table_sql!(),
+ ";\n",
+ publication_targets_schedule_sql!(),
+ ";\n",
+ publication_targets_guard_update_sql!(),
+ ";\n",
+ immutable_no_delete_sql!(
+ "publication_targets_no_delete",
+ "publication_targets",
+ "publication targets are retained"
+ ),
+ ";\n",
+ publication_attempts_table_sql!(),
+ ";\n",
+ immutable_no_update_sql!(
+ "publication_attempts_no_update",
+ "publication_attempts",
+ "publication attempts are immutable"
+ ),
+ ";\n",
+ immutable_no_delete_sql!(
+ "publication_attempts_no_delete",
+ "publication_attempts",
+ "publication attempts are retained"
+ ),
+ ";"
+);
+
const EVIDENCE_RECONCILIATIONS_TABLE_SHA256: [u8; 32] = [
0xaf, 0xed, 0xa3, 0xfb, 0xfb, 0x32, 0x6b, 0xd5, 0x27, 0x26, 0x42, 0x1d, 0x6c, 0x41, 0x5e, 0xff,
0xf9, 0x71, 0xf7, 0x47, 0x72, 0x5e, 0xbf, 0x8b, 0x34, 0x26, 0x8f, 0x89, 0x6e, 0xa1, 0x2d, 0x9b,
@@ -773,6 +1211,103 @@ const EVIDENCE_RECONCILIATION_SOURCES_NO_DELETE_SHA256: [u8; 32] = [
0x98, 0x0b, 0x4c, 0xdc, 0xa2, 0xde, 0xce, 0xfc, 0x06, 0x41, 0x86, 0x69, 0x2e, 0xac, 0x00, 0x39,
];
+const EVIDENCE_MANIFESTS_TABLE_SHA256: [u8; 32] = [
+ 0x95, 0x41, 0x37, 0x66, 0x31, 0x3e, 0x96, 0x83, 0x81, 0xdf, 0x0d, 0x8b, 0xc5, 0xd5, 0x50, 0x8b,
+ 0xde, 0x34, 0x3c, 0x68, 0xd6, 0x56, 0xea, 0xea, 0xab, 0x08, 0x87, 0x10, 0x9b, 0x23, 0xc6, 0xfb,
+];
+const EVIDENCE_MANIFESTS_NO_UPDATE_SHA256: [u8; 32] = [
+ 0x7f, 0xc1, 0x21, 0xaf, 0x5e, 0x9d, 0xc8, 0x32, 0xae, 0xc9, 0x98, 0x6d, 0x45, 0xc7, 0x68, 0xf3,
+ 0xc1, 0x76, 0x14, 0xbb, 0xf6, 0xbe, 0x41, 0x11, 0x6a, 0x7e, 0xa9, 0x22, 0x90, 0x27, 0x0e, 0xe5,
+];
+const EVIDENCE_MANIFESTS_NO_DELETE_SHA256: [u8; 32] = [
+ 0xd9, 0x9e, 0x5f, 0x55, 0xf7, 0xf2, 0x48, 0x5e, 0xfc, 0xc3, 0xb9, 0x11, 0x88, 0xf5, 0x8d, 0x3b,
+ 0xa0, 0x0c, 0x3e, 0xf7, 0x82, 0x3d, 0x88, 0xe0, 0x82, 0xc2, 0x60, 0xb8, 0x0f, 0xf4, 0xd6, 0xa4,
+];
+const TRADE_PROJECTIONS_TABLE_SHA256: [u8; 32] = [
+ 0xac, 0x04, 0x55, 0xd5, 0x1b, 0xed, 0xfb, 0x9b, 0x59, 0xfd, 0xc9, 0xea, 0x63, 0x1b, 0x85, 0x63,
+ 0x7a, 0x16, 0xf1, 0xb1, 0xfe, 0xad, 0x4d, 0x4c, 0xdf, 0xba, 0xad, 0xa3, 0xef, 0x85, 0x65, 0x43,
+];
+const TRADE_PROJECTIONS_NO_UPDATE_SHA256: [u8; 32] = [
+ 0x8f, 0xba, 0x6b, 0x06, 0x8e, 0xb8, 0x69, 0x84, 0x14, 0x4b, 0x64, 0x14, 0xbf, 0x8c, 0x4e, 0x95,
+ 0x47, 0x58, 0xaf, 0x09, 0x7d, 0xbb, 0x3a, 0xfe, 0x72, 0x06, 0x21, 0x00, 0x6a, 0x43, 0x32, 0xe0,
+];
+const TRADE_PROJECTIONS_NO_DELETE_SHA256: [u8; 32] = [
+ 0x94, 0x8a, 0x5c, 0xed, 0x5a, 0xaa, 0x0a, 0xb4, 0x90, 0x1c, 0xe4, 0x3a, 0xdb, 0x97, 0x69, 0xbf,
+ 0x5a, 0x19, 0x5d, 0x35, 0x95, 0xc9, 0x39, 0x54, 0x73, 0xe9, 0x6e, 0xea, 0x9c, 0x08, 0x26, 0xb2,
+];
+const ATTESTATION_REPORTS_TABLE_SHA256: [u8; 32] = [
+ 0x97, 0xc4, 0x83, 0x37, 0x56, 0x92, 0x23, 0x67, 0xb2, 0xb8, 0xd2, 0x00, 0xeb, 0xc9, 0x31, 0x0d,
+ 0xc8, 0xb9, 0x73, 0x38, 0xf6, 0xc5, 0x9c, 0xe5, 0xe5, 0x19, 0x87, 0x64, 0x19, 0x9d, 0x44, 0xd2,
+];
+const ATTESTATION_REPORTS_SUPERSESSION_SHA256: [u8; 32] = [
+ 0x57, 0xd5, 0x59, 0x2e, 0x2a, 0x7a, 0xd0, 0x03, 0x06, 0x42, 0x28, 0x16, 0x31, 0x8c, 0xe1, 0x25,
+ 0x30, 0x1c, 0xd3, 0x73, 0xff, 0xc3, 0x47, 0xf5, 0xf8, 0x65, 0xc0, 0x63, 0x73, 0x76, 0xad, 0x8e,
+];
+const ATTESTATION_REPORTS_NO_UPDATE_SHA256: [u8; 32] = [
+ 0x5f, 0x27, 0x19, 0x68, 0xe4, 0xd8, 0x32, 0x84, 0xe1, 0x04, 0x09, 0x37, 0x0b, 0xdc, 0x55, 0x9d,
+ 0xfa, 0x8f, 0xce, 0xa9, 0x0f, 0xd9, 0xd3, 0x47, 0xd4, 0xfa, 0xc8, 0x12, 0x53, 0x77, 0x17, 0x23,
+];
+const ATTESTATION_REPORTS_NO_DELETE_SHA256: [u8; 32] = [
+ 0xbe, 0xaf, 0xa1, 0x1a, 0xbe, 0x57, 0x27, 0xac, 0x3e, 0x43, 0x26, 0xd0, 0x8b, 0x0d, 0x39, 0x36,
+ 0xb3, 0x04, 0x11, 0x37, 0x48, 0x13, 0xd3, 0x2a, 0xc8, 0x24, 0x1f, 0x56, 0xa9, 0x2c, 0x51, 0xfc,
+];
+const SIGNED_ATTESTATION_EVENTS_TABLE_SHA256: [u8; 32] = [
+ 0x9b, 0x83, 0x5b, 0x84, 0x97, 0x1f, 0x52, 0xba, 0xc2, 0x8f, 0xf2, 0xba, 0x04, 0x9a, 0x1b, 0x9b,
+ 0xfc, 0xcc, 0x7c, 0xab, 0x39, 0xd0, 0x16, 0x53, 0x06, 0xa3, 0x9b, 0x93, 0x8c, 0x51, 0x72, 0xab,
+];
+const SIGNED_ATTESTATION_EVENTS_NO_UPDATE_SHA256: [u8; 32] = [
+ 0x36, 0x1d, 0xbe, 0xda, 0xae, 0xd4, 0xfc, 0x4c, 0xf3, 0xe8, 0xa8, 0x60, 0xeb, 0x63, 0xc8, 0xc7,
+ 0x3a, 0x31, 0x79, 0x7c, 0x4f, 0x92, 0x79, 0x8f, 0x7d, 0x3a, 0x9f, 0xec, 0x7f, 0x95, 0xa8, 0x44,
+];
+const SIGNED_ATTESTATION_EVENTS_NO_DELETE_SHA256: [u8; 32] = [
+ 0x06, 0x4d, 0x71, 0x90, 0x60, 0x9e, 0x6a, 0x8e, 0xac, 0x6d, 0x80, 0x44, 0xe1, 0xef, 0x53, 0x76,
+ 0x6a, 0xea, 0x6a, 0xb3, 0x68, 0xc9, 0x48, 0xb1, 0x64, 0x24, 0x9a, 0xf2, 0xc0, 0xc3, 0xeb, 0x9c,
+];
+const PUBLICATION_OUTBOX_TABLE_SHA256: [u8; 32] = [
+ 0x26, 0x2a, 0x56, 0x79, 0x77, 0x73, 0xcb, 0x84, 0xb6, 0x55, 0x1c, 0x19, 0x32, 0xe1, 0x0e, 0xb1,
+ 0x98, 0xf2, 0x4b, 0xf2, 0xb3, 0x5f, 0x90, 0x15, 0xac, 0xc1, 0x8f, 0x27, 0xfd, 0x89, 0x4b, 0x2d,
+];
+const PUBLICATION_OUTBOX_SCHEDULE_SHA256: [u8; 32] = [
+ 0xc0, 0x70, 0xb5, 0xb0, 0xd0, 0x1f, 0x05, 0x34, 0xe9, 0x1e, 0xfa, 0xee, 0x6c, 0xba, 0xdd, 0xf8,
+ 0x5c, 0xf1, 0x85, 0x33, 0xa7, 0x04, 0x5c, 0xfb, 0x65, 0x11, 0xdc, 0x80, 0x28, 0x7b, 0x4f, 0x6e,
+];
+const PUBLICATION_OUTBOX_GUARD_UPDATE_SHA256: [u8; 32] = [
+ 0x3e, 0xd0, 0x80, 0x98, 0x34, 0xb2, 0x24, 0x51, 0x5b, 0x7b, 0x06, 0x8e, 0x46, 0x8e, 0xbe, 0x8a,
+ 0x52, 0x8b, 0xdb, 0xcf, 0xb4, 0x0e, 0x33, 0xe5, 0x19, 0xc6, 0xfd, 0xef, 0x19, 0x80, 0xcd, 0x62,
+];
+const PUBLICATION_OUTBOX_NO_DELETE_SHA256: [u8; 32] = [
+ 0xc5, 0x77, 0x63, 0xa0, 0x90, 0xde, 0xe0, 0x11, 0x62, 0x2a, 0xda, 0x9f, 0x32, 0x24, 0x47, 0x59,
+ 0x54, 0xdf, 0x60, 0xd7, 0xe3, 0xf2, 0xf3, 0x42, 0x36, 0x14, 0x8e, 0xba, 0x52, 0x84, 0x3a, 0x6e,
+];
+const PUBLICATION_TARGETS_TABLE_SHA256: [u8; 32] = [
+ 0x23, 0xa0, 0x78, 0x7f, 0x7d, 0x90, 0x91, 0x8b, 0x41, 0x4a, 0x22, 0x37, 0xcc, 0x70, 0xa0, 0x83,
+ 0x1a, 0xe0, 0xfa, 0x05, 0x2e, 0x1b, 0x9f, 0x25, 0x50, 0xe3, 0x6f, 0xda, 0xd1, 0x53, 0xab, 0x7b,
+];
+const PUBLICATION_TARGETS_SCHEDULE_SHA256: [u8; 32] = [
+ 0xf8, 0xc4, 0x46, 0x10, 0xcb, 0x2d, 0xe4, 0xa3, 0x54, 0xd7, 0x93, 0x64, 0x9f, 0x8b, 0xa9, 0xf1,
+ 0x95, 0xfd, 0xba, 0x5d, 0xfc, 0xc1, 0xf9, 0x92, 0xe9, 0x08, 0x08, 0x7e, 0x56, 0x6a, 0x14, 0xde,
+];
+const PUBLICATION_TARGETS_GUARD_UPDATE_SHA256: [u8; 32] = [
+ 0x57, 0x10, 0x3b, 0xa2, 0xc3, 0xd7, 0x88, 0xd3, 0x83, 0x3a, 0xba, 0xed, 0xcd, 0xad, 0x49, 0x5e,
+ 0xce, 0xfe, 0x6e, 0xbd, 0x63, 0x2a, 0x98, 0x14, 0x04, 0x10, 0x86, 0xb9, 0x84, 0x10, 0x72, 0x0c,
+];
+const PUBLICATION_TARGETS_NO_DELETE_SHA256: [u8; 32] = [
+ 0x53, 0x97, 0x8a, 0xa5, 0xca, 0x4d, 0xef, 0x0e, 0xc4, 0x75, 0xc4, 0x55, 0xf6, 0x10, 0x43, 0xf7,
+ 0x1b, 0x10, 0x15, 0x6b, 0x2b, 0x56, 0xe0, 0x6c, 0x50, 0x4a, 0xc7, 0xee, 0x57, 0x3e, 0x74, 0x4e,
+];
+const PUBLICATION_ATTEMPTS_TABLE_SHA256: [u8; 32] = [
+ 0x7d, 0xa6, 0xea, 0xbe, 0xfc, 0x07, 0xeb, 0x16, 0xde, 0x5c, 0x47, 0xc9, 0x20, 0xfd, 0x64, 0x76,
+ 0xa9, 0xe9, 0x8d, 0xce, 0xe9, 0x31, 0x84, 0x45, 0x8d, 0xd2, 0x98, 0xb8, 0x53, 0x52, 0x03, 0x28,
+];
+const PUBLICATION_ATTEMPTS_NO_UPDATE_SHA256: [u8; 32] = [
+ 0xc3, 0xe5, 0x51, 0x35, 0x06, 0xad, 0x45, 0xca, 0xea, 0xe5, 0xaa, 0x7e, 0xa2, 0x40, 0xe7, 0x0a,
+ 0xde, 0x7c, 0xf7, 0x7f, 0x6d, 0x9a, 0x67, 0x76, 0x92, 0x5e, 0x12, 0x06, 0xc4, 0x55, 0xbf, 0x65,
+];
+const PUBLICATION_ATTEMPTS_NO_DELETE_SHA256: [u8; 32] = [
+ 0x6d, 0x8a, 0x4e, 0x03, 0x67, 0x52, 0x51, 0x1a, 0x4e, 0xe7, 0xaa, 0x41, 0x0c, 0x31, 0xaa, 0xc9,
+ 0xd3, 0x5c, 0x42, 0x48, 0xe3, 0x78, 0xfa, 0x39, 0x7c, 0x9b, 0x90, 0xa0, 0xe3, 0x72, 0x19, 0x29,
+];
+
const RECONCILIATION_JOBS_TABLE_SHA256: [u8; 32] = [
0xe7, 0x0b, 0x5c, 0xb7, 0x26, 0x91, 0x9d, 0x02, 0xef, 0xb3, 0xa6, 0x21, 0x58, 0x48, 0xce, 0x92,
0x30, 0x88, 0x17, 0x2b, 0x3f, 0xe0, 0xc1, 0x40, 0xee, 0x4a, 0xc7, 0x1b, 0xd0, 0x3c, 0x66, 0xa7,
@@ -989,16 +1524,24 @@ pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError>
MigrationChecksum::from_bytes(RHI_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256),
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
+ let report_publication = MigrationDescriptor::sql(
+ 7,
+ "create_reports_and_publication_outbox",
+ CREATE_REPORT_PUBLICATION_MIGRATION_SQL,
+ MigrationChecksum::from_bytes(RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
let catalog = MigrationCatalog::new([
configuration,
trade_evidence,
source_checkpoints,
reconciliation_jobs,
reconciliation_results,
+ report_publication,
])
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
if catalog.current_version() != RHI_STATE_SCHEMA_VERSION
- || catalog.descriptors().len() != 5
+ || catalog.descriptors().len() != 6
|| catalog.digest().as_bytes() != &RHI_MIGRATION_CATALOG_SHA256
{
return Err(RhiStateCatalogError::new(
@@ -1042,11 +1585,17 @@ pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
let version_six = SchemaVersionCatalog::new(
- RHI_STATE_SCHEMA_VERSION,
+ 6,
rhi_schema_version_six_objects()?,
SchemaDigest::from_bytes(RHI_STATE_SCHEMA_VERSION_6_SHA256),
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
+ let version_seven = SchemaVersionCatalog::new(
+ RHI_STATE_SCHEMA_VERSION,
+ rhi_schema_version_seven_objects()?,
+ SchemaDigest::from_bytes(RHI_STATE_SCHEMA_VERSION_7_SHA256),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
let catalog = SchemaCatalog::new(
&migrations,
[
@@ -1056,6 +1605,7 @@ pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
version_four,
version_five,
version_six,
+ version_seven,
],
)
.map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
@@ -1070,10 +1620,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() == 5
+ && migrations.descriptors().len() == 6
&& migrations.digest().as_bytes() == &RHI_MIGRATION_CATALOG_SHA256
&& schema.migration_catalog_digest() == migrations.digest()
- && versions.len() == 6
+ && versions.len() == 7
&& 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
@@ -1089,9 +1639,12 @@ pub fn validate_rhi_state_catalogs(
&& versions[4].version() == 5
&& versions[4].object_count() == RHI_STATE_SCHEMA_VERSION_5_OBJECT_COUNT
&& versions[4].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_5_SHA256
- && versions[5].version() == RHI_STATE_SCHEMA_VERSION
+ && versions[5].version() == 6
&& versions[5].object_count() == RHI_STATE_SCHEMA_VERSION_6_OBJECT_COUNT
&& versions[5].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_6_SHA256
+ && versions[6].version() == RHI_STATE_SCHEMA_VERSION
+ && versions[6].object_count() == RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT
+ && versions[6].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_7_SHA256
&& schema.digest().as_bytes() == &RHI_STATE_SCHEMA_CATALOG_SHA256;
if valid {
Ok(())
@@ -1163,6 +1716,195 @@ fn rhi_schema_version_six_objects() -> Result<Vec<SchemaObject>, RhiStateCatalog
Ok(objects)
}
+fn rhi_schema_version_seven_objects() -> Result<Vec<SchemaObject>, RhiStateCatalogError> {
+ let mut objects = rhi_schema_version_six_objects()?;
+ objects.extend(rhi_report_publication_objects()?);
+ Ok(objects)
+}
+
+fn rhi_report_publication_objects() -> Result<[SchemaObject; 24], RhiStateCatalogError> {
+ let object = |kind, name, table_name, sql, digest| {
+ SchemaObject::new(
+ kind,
+ name,
+ table_name,
+ sql,
+ SchemaDigest::from_bytes(digest),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))
+ };
+ Ok([
+ object(
+ SchemaObjectKind::Table,
+ "evidence_manifests",
+ "evidence_manifests",
+ CREATE_EVIDENCE_MANIFESTS_TABLE_SQL,
+ EVIDENCE_MANIFESTS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "evidence_manifests_no_update",
+ "evidence_manifests",
+ CREATE_EVIDENCE_MANIFESTS_NO_UPDATE_SQL,
+ EVIDENCE_MANIFESTS_NO_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "evidence_manifests_no_delete",
+ "evidence_manifests",
+ CREATE_EVIDENCE_MANIFESTS_NO_DELETE_SQL,
+ EVIDENCE_MANIFESTS_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "trade_projections",
+ "trade_projections",
+ CREATE_TRADE_PROJECTIONS_TABLE_SQL,
+ TRADE_PROJECTIONS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "trade_projections_no_update",
+ "trade_projections",
+ CREATE_TRADE_PROJECTIONS_NO_UPDATE_SQL,
+ TRADE_PROJECTIONS_NO_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "trade_projections_no_delete",
+ "trade_projections",
+ CREATE_TRADE_PROJECTIONS_NO_DELETE_SQL,
+ TRADE_PROJECTIONS_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "attestation_reports",
+ "attestation_reports",
+ CREATE_ATTESTATION_REPORTS_TABLE_SQL,
+ ATTESTATION_REPORTS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Index,
+ "attestation_reports_one_successor",
+ "attestation_reports",
+ CREATE_ATTESTATION_REPORTS_SUPERSESSION_SQL,
+ ATTESTATION_REPORTS_SUPERSESSION_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "attestation_reports_no_update",
+ "attestation_reports",
+ CREATE_ATTESTATION_REPORTS_NO_UPDATE_SQL,
+ ATTESTATION_REPORTS_NO_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "attestation_reports_no_delete",
+ "attestation_reports",
+ CREATE_ATTESTATION_REPORTS_NO_DELETE_SQL,
+ ATTESTATION_REPORTS_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "signed_attestation_events",
+ "signed_attestation_events",
+ CREATE_SIGNED_ATTESTATION_EVENTS_TABLE_SQL,
+ SIGNED_ATTESTATION_EVENTS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "signed_attestation_events_no_update",
+ "signed_attestation_events",
+ CREATE_SIGNED_ATTESTATION_EVENTS_NO_UPDATE_SQL,
+ SIGNED_ATTESTATION_EVENTS_NO_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "signed_attestation_events_no_delete",
+ "signed_attestation_events",
+ CREATE_SIGNED_ATTESTATION_EVENTS_NO_DELETE_SQL,
+ SIGNED_ATTESTATION_EVENTS_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "publication_outbox",
+ "publication_outbox",
+ CREATE_PUBLICATION_OUTBOX_TABLE_SQL,
+ PUBLICATION_OUTBOX_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Index,
+ "publication_outbox_by_schedule",
+ "publication_outbox",
+ CREATE_PUBLICATION_OUTBOX_SCHEDULE_SQL,
+ PUBLICATION_OUTBOX_SCHEDULE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_outbox_guard_update",
+ "publication_outbox",
+ CREATE_PUBLICATION_OUTBOX_GUARD_UPDATE_SQL,
+ PUBLICATION_OUTBOX_GUARD_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_outbox_no_delete",
+ "publication_outbox",
+ CREATE_PUBLICATION_OUTBOX_NO_DELETE_SQL,
+ PUBLICATION_OUTBOX_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "publication_targets",
+ "publication_targets",
+ CREATE_PUBLICATION_TARGETS_TABLE_SQL,
+ PUBLICATION_TARGETS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Index,
+ "publication_targets_by_schedule",
+ "publication_targets",
+ CREATE_PUBLICATION_TARGETS_SCHEDULE_SQL,
+ PUBLICATION_TARGETS_SCHEDULE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_targets_guard_update",
+ "publication_targets",
+ CREATE_PUBLICATION_TARGETS_GUARD_UPDATE_SQL,
+ PUBLICATION_TARGETS_GUARD_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_targets_no_delete",
+ "publication_targets",
+ CREATE_PUBLICATION_TARGETS_NO_DELETE_SQL,
+ PUBLICATION_TARGETS_NO_DELETE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Table,
+ "publication_attempts",
+ "publication_attempts",
+ CREATE_PUBLICATION_ATTEMPTS_TABLE_SQL,
+ PUBLICATION_ATTEMPTS_TABLE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_attempts_no_update",
+ "publication_attempts",
+ CREATE_PUBLICATION_ATTEMPTS_NO_UPDATE_SQL,
+ PUBLICATION_ATTEMPTS_NO_UPDATE_SHA256,
+ )?,
+ object(
+ SchemaObjectKind::Trigger,
+ "publication_attempts_no_delete",
+ "publication_attempts",
+ CREATE_PUBLICATION_ATTEMPTS_NO_DELETE_SQL,
+ PUBLICATION_ATTEMPTS_NO_DELETE_SHA256,
+ )?,
+ ])
+}
+
fn rhi_reconciliation_result_objects() -> Result<[SchemaObject; 6], RhiStateCatalogError> {
let object = |kind, name, table_name, sql, digest| {
SchemaObject::new(
diff --git a/tests/build_policy.rs b/tests/build_policy.rs
@@ -27,7 +27,7 @@ fn source_lock_metadata_is_exact_and_nix_is_absent() {
));
for field in [
"config_contract_version = 1",
- "state_contract_version = 6",
+ "state_contract_version = 7",
"admin_contract_version = 1",
"status_contract_version = 1",
"provider_contract_version = 1",
@@ -93,7 +93,7 @@ fn source_lock_binds_the_current_cargo_lock() {
assert!(!SOURCE_LOCK.contains("flake_lock_sha256"));
assert!(!SOURCE_LOCK.contains("lib_revision ="));
assert!(SOURCE_LOCK.ends_with(
- "[contract_versions]\nconfig = 1\nstate = 6\nadmin = 1\nstatus = 1\nprovider = 1\n"
+ "[contract_versions]\nconfig = 1\nstate = 7\nadmin = 1\nstatus = 1\nprovider = 1\n"
));
}
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 PUBLICATION: &str = include_str!("../src/publication.rs");
+const PUBLICATION_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/publication_outbox.v1.json");
const RECONCILIATION_ATTEMPTS: &str = include_str!("../src/reconciliation_attempt.rs");
const RECONCILIATION_COMMIT: &str = include_str!("../src/reconciliation_commit.rs");
const RECONCILIATION_ATTESTATION: &str = include_str!("../src/reconciliation_attestation.rs");
@@ -51,6 +54,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/publication.rs"),
include_str!("../src/reconciliation_attempt.rs"),
include_str!("../src/reconciliation_attestation.rs"),
include_str!("../src/reconciliation_commit.rs"),
@@ -125,6 +129,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"features",
"identity_credential",
"identity_envelope",
+ "publication",
"reconciliation_attempt",
"reconciliation_attestation",
"reconciliation_commit",
@@ -168,6 +173,12 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"validate_rhi_state_catalogs",
"RhiStateCatalogError",
"RhiRuntimeAdapters",
+ "RhiPublicationAuthority",
+ "RhiPublicationErrorKind",
+ "RhiPublicationMode",
+ "RhiPublicationRetryPolicy",
+ "RhiPublicationTarget",
+ "RHI_PUBLICATION_CONTRACT_VERSION",
"RhiReconciliationAttemptPlan",
"RhiReconciliationSourceRequest",
"RhiReconciliationSourceResult",
@@ -250,6 +261,44 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
assert!(!PUBLIC_API.contains("rhi::adapters::"));
assert!(!PUBLIC_API.contains("rhi::features::"));
assert!(!PUBLIC_API.contains("rhi::runtime_adapters::"));
+ assert!(!PUBLIC_API.contains("rhi::publication::"));
+}
+
+#[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");
+ assert_eq!(contract["schema"], "radroots.rhi.publication-outbox");
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(contract["state_schema_version"], 7);
+ assert_eq!(contract["effects"]["sqlite_query_or_mutation"], false);
+ assert_eq!(contract["effects"]["relay_or_network"], false);
+ for required in [
+ "pub fn from_config(config: &RhiConfigDocumentV1)",
+ "RhiPublicationMode::Required",
+ "RhiPublicationMode::Disabled",
+ "target_set_digest(&targets)?",
+ "authority_digest(",
+ ] {
+ assert!(
+ PUBLICATION.contains(required),
+ "publication authority is missing {required}"
+ );
+ }
+ for forbidden in [
+ "sqlx::",
+ "std::fs",
+ "std::net",
+ "tokio::",
+ "SystemTime",
+ "thread_rng",
+ "OsRng",
+ ] {
+ assert!(
+ !PUBLICATION.contains(forbidden),
+ "publication authority gained forbidden effect {forbidden}"
+ );
+ }
}
#[test]
@@ -503,7 +552,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, 24);
+ assert_eq!(public_error_count, 25);
}
#[test]
@@ -932,6 +981,8 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"Step 199 must rerun the same validator inside the final",
"## Canonical signed reconciliation attestation",
"[`reconciliation_attestation.v1.json`](contracts/services_hardening/reconciliation_attestation.v1.json)",
+ "## Explicit publication authority and durable schema",
+ "[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)",
"The event body and exact kind-3441 structural tags",
"without rebuilding, reserializing, or",
"Coverage is exactly `Missing`, `Partial`, `ScopeSatisfied`, or `Unsupported`",
@@ -949,6 +1000,9 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"4,096 distinct signed-event identities (event ID plus signature) and 8 MiB",
"observation, and operational retry do not",
"schema-v6 immutable",
+ "schema-v7 immutable report, signed-event,",
+ "The schema-v7",
+ "catalog is frozen by Step 198",
"one canonical mutation",
"every distinct valid signed",
"does not advance reconciliation checkpoints or dirty generation",
diff --git a/tests/services_hardening_publication_contract.rs b/tests/services_hardening_publication_contract.rs
@@ -0,0 +1,185 @@
+#![forbid(unsafe_code)]
+
+use std::error::Error;
+
+use rhi::{
+ RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS,
+ RHI_STATE_SCHEMA_VERSION, RhiConfigProfile, RhiPublicationAuthority, RhiPublicationMode,
+ parse_rhi_config_v1,
+};
+use serde_json::json;
+
+const CONTRACT: &str = include_str!("../contracts/services_hardening/publication_outbox.v1.json");
+const EXAMPLE: &[u8] = include_bytes!("../contracts/services_hardening/config.v1.example.toml");
+const LIB_SOURCE: &str = include_str!("../src/lib.rs");
+const PUBLICATION_SOURCE: &str = include_str!("../src/publication.rs");
+const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
+const README: &str = include_str!("../README");
+
+#[test]
+fn machine_contract_freezes_step_198_authority_and_schema() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
+ assert_eq!(contract["schema"], "radroots.rhi.publication-outbox");
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(
+ contract["contract_version"],
+ RHI_PUBLICATION_CONTRACT_VERSION
+ );
+ assert_eq!(contract["state_schema_version"], RHI_STATE_SCHEMA_VERSION);
+ assert_eq!(
+ contract["publication_modes"],
+ json!(["required", "disabled"])
+ );
+ assert_eq!(
+ contract["authority"]["required"]["target_count"]["maximum"],
+ RHI_PUBLICATION_MAX_TARGETS
+ );
+ assert_eq!(
+ contract["schema_objects"]["immutable"],
+ json!([
+ "evidence_manifests",
+ "trade_projections",
+ "attestation_reports",
+ "signed_attestation_events",
+ "publication_attempts"
+ ])
+ );
+ assert_eq!(
+ contract["schema_objects"]["compare_and_swap"],
+ json!(["publication_outbox", "publication_targets"])
+ );
+ assert_eq!(
+ contract["schema_objects"]["target_states"],
+ json!([
+ "pending",
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown"
+ ])
+ );
+ assert_eq!(contract["payload"]["signed_bytes_maximum"], 32_768);
+ assert_eq!(contract["effects"]["sqlite_query_or_mutation"], false);
+ assert_eq!(contract["effects"]["relay_or_network"], false);
+ for forbidden in [
+ "implicit_publication_mode",
+ "disabled_mode_target_or_retry_state",
+ "caller_forged_target",
+ "non_write_relay_target",
+ "mutable_signed_payload",
+ "mutable_target_identity",
+ "raw_upstream_error_text",
+ "raw_sqlite_handle",
+ "relay_io",
+ "event_rebuild",
+ "event_reserialize",
+ "event_resign",
+ "unbounded_queue_or_target_inventory",
+ ] {
+ assert!(
+ contract["forbidden"]
+ .as_array()
+ .expect("forbidden")
+ .iter()
+ .any(|value| value == forbidden),
+ "missing forbidden boundary {forbidden}"
+ );
+ }
+}
+
+#[test]
+fn canonical_example_has_literal_publication_identities() {
+ let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
+ let authority = RhiPublicationAuthority::from_config(&config).expect("authority");
+ assert_eq!(authority.mode(), RhiPublicationMode::Required);
+ assert_eq!(authority.targets().len(), 2);
+ let retry = authority.retry_policy().expect("retry");
+ assert_eq!(retry.maximum_attempts(), 10);
+ assert!(retry.maximum_attempts() <= RHI_PUBLICATION_MAX_ATTEMPTS);
+ assert_eq!(
+ lower_hex(authority.target_set_sha256()),
+ "fc044570890935bc41f4763b92d0e079ea0288e0a77a03fc1e299a4c830bbd76"
+ );
+ assert_eq!(
+ lower_hex(authority.authority_sha256()),
+ "6df5c3bf1bbb5c7b57d81bd1ee600677501a05d6e3c3aaf7577d64ffcbd9566b"
+ );
+}
+
+#[test]
+fn module_and_side_effect_authority_remain_private_and_deferred() {
+ assert!(LIB_SOURCE.contains("mod publication;"));
+ assert!(!LIB_SOURCE.contains("pub mod publication;"));
+ assert!(LIB_SOURCE.contains("RhiPublicationAuthority"));
+ assert!(README.contains("## Explicit publication authority and durable schema"));
+ assert!(README.contains(
+ "[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)"
+ ));
+ for table in [
+ "evidence_manifests",
+ "trade_projections",
+ "attestation_reports",
+ "signed_attestation_events",
+ "publication_outbox",
+ "publication_targets",
+ "publication_attempts",
+ ] {
+ assert!(CATALOG_SOURCE.contains(&format!("CREATE TABLE {table}")));
+ }
+ assert!(CATALOG_SOURCE.contains("OR OLD.state = 'accepted'"));
+ assert!(CATALOG_SOURCE.contains("AND next_attempt_unix_ms IS NOT NULL"));
+ assert!(CATALOG_SOURCE.contains("AND lease_owner IS NOT NULL"));
+ assert!(CATALOG_SOURCE.contains("AND lease_expires_unix_ms IS NOT NULL"));
+ for forbidden in [
+ "sqlx::",
+ "radroots_transport",
+ "PublicationSink",
+ "SystemTime",
+ "thread_rng",
+ "OsRng",
+ "std::fs",
+ "std::net",
+ "tokio::spawn",
+ "spawn_blocking",
+ "pub fn sqlite",
+ "pub fn transaction",
+ "pub fn connection",
+ "pub fn into_inner",
+ ] {
+ assert!(
+ !PUBLICATION_SOURCE.contains(forbidden),
+ "premature publication authority {forbidden}"
+ );
+ }
+}
+
+#[test]
+fn public_authority_debug_and_errors_reveal_no_targets_or_digests() {
+ let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
+ let authority = RhiPublicationAuthority::from_config(&config).expect("authority");
+ let rendered = format!("{authority:?}");
+ assert!(!rendered.contains("relay-primary"));
+ assert!(!rendered.contains(&lower_hex(authority.authority_sha256())));
+
+ let invalid = core::str::from_utf8(EXAMPLE)
+ .expect("utf8")
+ .replace("mode = \"required\"", "mode = \"invalid-secret\"");
+ let error = parse_rhi_config_v1(invalid.as_bytes(), RhiConfigProfile::Production)
+ .expect_err("invalid config");
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains("invalid-secret"));
+ assert!(Error::source(&error).is_none());
+}
+
+fn lower_hex(bytes: &[u8]) -> String {
+ const DIGITS: &[u8; 16] = b"0123456789abcdef";
+ let mut output = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ output.push(char::from(DIGITS[usize::from(byte >> 4)]));
+ output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
+ }
+ output
+}
diff --git a/tests/services_hardening_state_catalog.rs b/tests/services_hardening_state_catalog.rs
@@ -17,8 +17,9 @@ use rhi::{
RHI_STATE_SCHEMA_VERSION_5_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_5_OBJECT_COUNT,
RHI_STATE_SCHEMA_VERSION_5_SHA256, RHI_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256,
RHI_STATE_SCHEMA_VERSION_6_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_6_SHA256,
- RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
- validate_rhi_state_catalogs,
+ RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256, RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT,
+ RHI_STATE_SCHEMA_VERSION_7_SHA256, RhiStateCatalogErrorKind, rhi_migration_catalog,
+ rhi_schema_catalog, validate_rhi_state_catalogs,
};
const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
@@ -26,14 +27,14 @@ const LIB_SOURCE: &str = include_str!("../src/lib.rs");
const MANIFEST: &str = include_str!("../Cargo.toml");
#[test]
-fn schema_v1_through_v6_catalogs_have_exact_literal_identities() {
+fn schema_v1_through_v7_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, 6);
- assert_eq!(migrations.descriptors().len(), 5);
- assert_eq!(migrations.current_version(), 6);
+ assert_eq!(RHI_STATE_SCHEMA_VERSION, 7);
+ assert_eq!(migrations.descriptors().len(), 6);
+ assert_eq!(migrations.current_version(), 7);
assert_eq!(migrations.descriptors()[0].target_version(), 2);
assert_eq!(
migrations.descriptors()[0].name().as_str(),
@@ -79,12 +80,21 @@ fn schema_v1_through_v6_catalogs_have_exact_literal_identities() {
migrations.descriptors()[4].checksum().as_bytes(),
&RHI_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256
);
+ assert_eq!(migrations.descriptors()[5].target_version(), 7);
+ assert_eq!(
+ migrations.descriptors()[5].name().as_str(),
+ "create_reports_and_publication_outbox"
+ );
+ assert_eq!(
+ migrations.descriptors()[5].checksum().as_bytes(),
+ &RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256
+ );
assert_eq!(
migrations.digest().as_bytes(),
&RHI_MIGRATION_CATALOG_SHA256
);
- assert_eq!(schema.versions().len(), 6);
+ assert_eq!(schema.versions().len(), 7);
let version = schema.versions()[0];
assert_eq!(version.version(), 1);
assert_eq!(
@@ -151,13 +161,24 @@ fn schema_v1_through_v6_catalogs_have_exact_literal_identities() {
version.digest().as_bytes(),
&RHI_STATE_SCHEMA_VERSION_6_SHA256
);
+ let version = schema.versions()[6];
+ assert_eq!(version.version(), 7);
+ assert_eq!(
+ version.object_count(),
+ RHI_STATE_SCHEMA_VERSION_7_OBJECT_COUNT
+ );
+ assert_eq!(version.object_count(), 63);
+ assert_eq!(
+ version.digest().as_bytes(),
+ &RHI_STATE_SCHEMA_VERSION_7_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),
- "32f49f1c50f54c722d94d9343a052e67142d00747a0bb1cc1cb5cabafa30fe4c"
+ "bf955884e973de04b98a57986562ef3805ad82ce3da6a83ab5890a50e06bd5f4"
);
assert_eq!(
lower_hex(&RHI_STATE_SCHEMA_VERSION_1_SHA256),
@@ -204,8 +225,16 @@ fn schema_v1_through_v6_catalogs_have_exact_literal_identities() {
"5d1fa9508b5b8a0c8065fd37f11c6866d51f9294c75272041f349e95ced50fb4"
);
assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256),
+ "9debf4f3caad4d018311cb169bf9282264d6abfc49e718840b9cbfad42ed357c"
+ );
+ assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_VERSION_7_SHA256),
+ "840aa83c689f9df99d26c5b4ef117131c270ef7522674037def79628a2b03946"
+ );
+ assert_eq!(
lower_hex(&RHI_STATE_SCHEMA_CATALOG_SHA256),
- "8c1982b295d6544bbe6df679424ee6caf3476831b38de9946b115fbc5b246b03"
+ "ec31809d6207fd98b204e56939731197f29787bd1fef628d9b345db020711fec"
);
}
@@ -261,6 +290,10 @@ fn independent_validator_rejects_migration_or_schema_drift() {
SchemaVersionCatalog::computed_digest(6, [version_two_object()]).expect("v6 digest");
let version_six =
SchemaVersionCatalog::new(6, [version_two_object()], snapshot_digest).expect("version six");
+ let snapshot_digest =
+ SchemaVersionCatalog::computed_digest(7, [version_two_object()]).expect("v7 digest");
+ let version_seven = SchemaVersionCatalog::new(7, [version_two_object()], snapshot_digest)
+ .expect("version seven");
let schema = SchemaCatalog::new(
&exact_migrations,
[
@@ -270,6 +303,7 @@ fn independent_validator_rejects_migration_or_schema_drift() {
version_four,
version_five,
version_six,
+ version_seven,
],
)
.expect("drift schema catalog");
@@ -324,6 +358,10 @@ fn catalog_errors_are_stable_source_free_and_redacted() {
SchemaVersionCatalog::computed_digest(6, [secret_object()]).expect("v6 digest");
let version_six =
SchemaVersionCatalog::new(6, [secret_object()], version_six_digest).expect("version six");
+ let version_seven_digest =
+ SchemaVersionCatalog::computed_digest(7, [secret_object()]).expect("v7 digest");
+ let version_seven = SchemaVersionCatalog::new(7, [secret_object()], version_seven_digest)
+ .expect("version seven");
let schema = SchemaCatalog::new(
&migrations,
[
@@ -333,6 +371,7 @@ fn catalog_errors_are_stable_source_free_and_redacted() {
version_four,
version_five,
version_six,
+ version_seven,
],
)
.expect("schema catalog");
diff --git a/tests/services_hardening_state_host.rs b/tests/services_hardening_state_host.rs
@@ -11,6 +11,7 @@ use rhi::{
initialize_rhi_state, open_rhi_state_inspection, open_rhi_state_read_write,
parse_rhi_cli_v1_from, parse_rhi_config_v1, resolve_rhi_runtime_context,
};
+use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions};
const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
@@ -210,6 +211,100 @@ async fn initialize_is_create_new_and_both_existing_open_modes_close_explicitly(
}
#[tokio::test]
+async fn publication_schema_rejects_null_state_holes_and_accepted_target_mutation() {
+ let directory = tempfile::tempdir().expect("temporary root");
+ let runtime = runtime(directory.path(), "publication-schema");
+ prepare_state_directory(&runtime);
+ let metadata = metadata(&runtime);
+ let (applied_at, build) = migration_evidence();
+ initialize_rhi_state(&runtime, &metadata, applied_at, &build)
+ .await
+ .expect("state initialization");
+
+ let options = SqliteConnectOptions::new()
+ .filename(runtime.artifacts().state_database())
+ .create_if_missing(false)
+ .foreign_keys(false);
+ let mut connection = SqliteConnection::connect_with(&options)
+ .await
+ .expect("offline fixture connection");
+
+ let outbox_id = [0x21_u8; 32];
+ let event_id = [0x22_u8; 32];
+ let event_sha256 = [0x23_u8; 32];
+ let authority_sha256 = [0x24_u8; 32];
+ let target_set_sha256 = [0x25_u8; 32];
+ let missing_pending_schedule = sqlx::query(
+ r#"INSERT INTO publication_outbox (
+ outbox_id, event_id, event_sha256, publication_authority_sha256,
+ target_set_sha256, target_count, required_target_count,
+ max_attempts, initial_backoff_ms, maximum_backoff_ms,
+ attempt_deadline_ms, state, revision, next_attempt_unix_ms,
+ lease_owner, lease_expires_unix_ms, created_at_unix_ms,
+ updated_at_unix_ms
+ ) VALUES (?, ?, ?, ?, ?, 1, 1, 3, 100, 1000, 5000,
+ 'pending', 1, NULL, NULL, NULL, 10, 10)"#,
+ )
+ .bind(outbox_id.as_slice())
+ .bind(event_id.as_slice())
+ .bind(event_sha256.as_slice())
+ .bind(authority_sha256.as_slice())
+ .bind(target_set_sha256.as_slice())
+ .execute(&mut connection)
+ .await;
+ assert!(
+ missing_pending_schedule.is_err(),
+ "pending outbox rows require a concrete next-attempt time"
+ );
+
+ sqlx::query(
+ r#"INSERT INTO publication_outbox (
+ outbox_id, event_id, event_sha256, publication_authority_sha256,
+ target_set_sha256, target_count, required_target_count,
+ max_attempts, initial_backoff_ms, maximum_backoff_ms,
+ attempt_deadline_ms, state, revision, next_attempt_unix_ms,
+ lease_owner, lease_expires_unix_ms, created_at_unix_ms,
+ updated_at_unix_ms
+ ) VALUES (?, ?, ?, ?, ?, 1, 1, 3, 100, 1000, 5000,
+ 'pending', 1, 10, NULL, NULL, 10, 10)"#,
+ )
+ .bind(outbox_id.as_slice())
+ .bind(event_id.as_slice())
+ .bind(event_sha256.as_slice())
+ .bind(authority_sha256.as_slice())
+ .bind(target_set_sha256.as_slice())
+ .execute(&mut connection)
+ .await
+ .expect("valid pending outbox row");
+
+ sqlx::query(
+ r#"INSERT INTO publication_targets (
+ outbox_id, target_ordinal, relay_id, required, state, revision,
+ attempt_count, next_attempt_unix_ms, last_attempt_id,
+ updated_at_unix_ms
+ ) VALUES (?, 0, 'relay_a', 1, 'accepted', 1, 0, NULL, NULL, 10)"#,
+ )
+ .bind(outbox_id.as_slice())
+ .execute(&mut connection)
+ .await
+ .expect("accepted target fixture");
+ let accepted_mutation = sqlx::query(
+ r#"UPDATE publication_targets
+ SET revision = 2, updated_at_unix_ms = 11
+ WHERE outbox_id = ? AND target_ordinal = 0"#,
+ )
+ .bind(outbox_id.as_slice())
+ .execute(&mut connection)
+ .await;
+ assert!(
+ accepted_mutation.is_err(),
+ "accepted publication targets are terminal"
+ );
+
+ connection.close().await.expect("fixture connection close");
+}
+
+#[tokio::test]
async fn missing_state_and_mismatched_evidence_fail_before_database_creation() {
let directory = tempfile::tempdir().expect("temporary root");
let primary = runtime(directory.path(), "primary");
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 (7, 'unexpected_schema', ?, 1725000000, '0.1.0', ?, ?,
+ ) VALUES (8, 'unexpected_schema', ?, 1725000000, '0.1.0', ?, ?,
'rustc-test', 'test-target', 'service-host', 1, 7, 1, 1, 1)",
)
.bind([0x44_u8; 32].as_slice())
diff --git a/tests/source_guards.rs b/tests/source_guards.rs
@@ -186,6 +186,31 @@ fn rhi_wave_one_removes_prototype_runtime_and_selection_authority() {
}
}
+#[test]
+fn step_198_publication_boundary_has_no_runtime_or_storage_authority() {
+ let source = read_repo_file("src/publication.rs");
+ let root = read_repo_file("src/lib.rs");
+
+ assert!(root.contains("mod publication;"));
+ assert!(!root.contains("pub mod publication;"));
+ for forbidden in [
+ "sqlx::",
+ "radroots_transport",
+ "std::fs",
+ "std::net",
+ "tokio::",
+ "SystemTime",
+ "thread_rng",
+ "OsRng",
+ "PublicationSink",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "Step 198 publication authority gained deferred behavior `{forbidden}`"
+ );
+ }
+}
+
fn read_repo_file(relative_path: &str) -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
fs::read_to_string(path.as_path())