commit 253c072f820c6a71f4fec4187a1537227196176a
parent f03279a8e7f7bd27f234bc0e0a97efeeb9e4bfe5
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 08:43:13 +0000
rhi: reduce sealed reconciliation manifests
- bind the shared reducer to confirmed manifest material
- retain bounded immutable mutation content without public exposure
- freeze projection digests and redacted failure classifications
- qualify reducer contracts, API surface, and persistence compatibility
Diffstat:
13 files changed, 778 insertions(+), 8 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -143,6 +143,12 @@
- Keep Step 191 manifest materialization pure and in memory. Step 199 alone
owns durable manifest persistence; reducers, final coverage/outcome,
attestation, publication, and job finalization retain their ordered owners.
+- Reduce only the sealed owned reconciliation manifest. Retain its bounded
+ canonical mutation material privately from the confirmed Step 190 commit,
+ map its already-governed evidence coverage into the shared reducer input,
+ and bind the canonical shared projection digest to the exact manifest and
+ evidence-policy digests. Do not accept caller mutation material or add
+ SQLite, filesystem, source, relay, task, clock, entropy, or network access.
- Coverage is exactly `Missing`, `Partial`, `ScopeSatisfied`, or `Unsupported`.
ScopeSatisfied means only that the configured policy was satisfied; optional
evidence never substitutes for required-source completion.
diff --git a/README b/README
@@ -249,6 +249,23 @@ lease expiry/reclaim, queue and result bounds, stale leases/generations, and
durable reopen behavior without adding runtime, scheduler, source, or network
authority.
+## Pure reconciliation reducer
+
+[`reconciliation_reducer.v1.json`](contracts/services_hardening/reconciliation_reducer.v1.json)
+binds the promoted shared `radroots.trade.reducer.v1` to one sealed confirmed
+reconciliation manifest. The manifest privately retains only the bounded
+canonical mutation material derived from its Step 190 commit; callers cannot
+inject or replace reducer inputs. Reduction consumes the owned manifest,
+retains it inside a sealed projection, and binds the shared projection digest
+to the manifest and evidence-policy digests with a separate domain-separated
+RHI digest. The projection retains the canonical shared result privately for
+later checkpoints while exposing only bounded identity, digest, and count
+evidence.
+Reduction performs no SQLite, filesystem, source, relay, network, task, clock,
+or entropy operation. Final four-state coverage, three-state outcome,
+generation-fenced persistence, reports, attestations, and publication retain
+their later checkpoint owners.
+
## Existing-state runtime foundation
`open_rhi_runtime_foundation` opens only an already initialized database from
diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt
@@ -183,6 +183,11 @@ pub rhi::RhiReconciliationManifestErrorKind::InvalidCommittedInventory
pub rhi::RhiReconciliationManifestErrorKind::InvalidObservationTime
impl rhi::RhiReconciliationManifestErrorKind
pub const fn rhi::RhiReconciliationManifestErrorKind::code(self) -> &'static str
+pub enum rhi::RhiReconciliationReducerErrorKind
+pub rhi::RhiReconciliationReducerErrorKind::InvalidManifest
+pub rhi::RhiReconciliationReducerErrorKind::ProjectionUnavailable
+impl rhi::RhiReconciliationReducerErrorKind
+pub const fn rhi::RhiReconciliationReducerErrorKind::code(self) -> &'static str
pub enum rhi::RhiReconciliationReplayErrorKind
pub rhi::RhiReconciliationReplayErrorKind::InvalidConfiguration
pub rhi::RhiReconciliationReplayErrorKind::InvalidInput
@@ -742,6 +747,28 @@ impl core::fmt::Debug for rhi::RhiReconciliationManifestError
pub fn rhi::RhiReconciliationManifestError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for rhi::RhiReconciliationManifestError
pub fn rhi::RhiReconciliationManifestError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiReconciliationProjection
+impl rhi::RhiReconciliationProjection
+pub const fn rhi::RhiReconciliationProjection::contract_version(&self) -> u32
+pub const fn rhi::RhiReconciliationProjection::digest(&self) -> [u8; 32]
+pub fn rhi::RhiReconciliationProjection::issue_count(&self) -> usize
+pub const fn rhi::RhiReconciliationProjection::manifest(&self) -> &rhi::RhiReconciliationManifest
+pub const fn rhi::RhiReconciliationProjection::root_mutation_id(&self) -> core::option::Option<&radroots_event::id::MutationId>
+pub const fn rhi::RhiReconciliationProjection::shared_projection_digest(&self) -> [u8; 32]
+pub const fn rhi::RhiReconciliationProjection::shared_reducer_contract_id(&self) -> &'static str
+pub const fn rhi::RhiReconciliationProjection::shared_reducer_contract_version(&self) -> u16
+pub const fn rhi::RhiReconciliationProjection::trade_id(&self) -> &radroots_event::id::TradeId
+impl core::fmt::Debug for rhi::RhiReconciliationProjection
+pub fn rhi::RhiReconciliationProjection::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiReconciliationReducerError
+impl rhi::RhiReconciliationReducerError
+pub const fn rhi::RhiReconciliationReducerError::code(self) -> &'static str
+pub const fn rhi::RhiReconciliationReducerError::kind(self) -> rhi::RhiReconciliationReducerErrorKind
+impl core::error::Error for rhi::RhiReconciliationReducerError
+impl core::fmt::Debug for rhi::RhiReconciliationReducerError
+pub fn rhi::RhiReconciliationReducerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiReconciliationReducerError
+pub fn rhi::RhiReconciliationReducerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiReconciliationReplayError
impl rhi::RhiReconciliationReplayError
pub const fn rhi::RhiReconciliationReplayError::code(self) -> &'static str
@@ -1225,6 +1252,7 @@ pub const rhi::RHI_RECONCILIATION_COMMIT_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_JOB_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_JOB_MAX_ACTIVE: u32
pub const rhi::RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION: u32
+pub const rhi::RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_REPLAY_CONTRACT_VERSION: u32
pub const rhi::RHI_RUNTIME_ADAPTER_CONTRACT_VERSION: u32
pub const rhi::RHI_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32
@@ -1288,6 +1316,7 @@ pub async fn rhi::open_rhi_state_read_write_from_config(&rhi::RhiRuntimeContext,
pub fn rhi::parse_rhi_cli_v1_from<I, T>(I) -> core::result::Result<rhi::RhiCliInvocationV1, rhi::RhiCliV1Error> where I: core::iter::traits::collect::IntoIterator<Item = T>, T: core::convert::Into<std::ffi::os_str::OsString> + core::clone::Clone
pub fn rhi::parse_rhi_config_v1(&[u8], rhi::RhiConfigProfile) -> core::result::Result<rhi::RhiConfigDocumentV1, rhi::RhiConfigV1Error>
pub fn rhi::provision_rhi_encrypted_identity(&rhi::RhiIdentityEnvelopeBinding, &rhi::RhiWrappingCredential, rhi::RhiEncryptedIdentityProvisioningMaterial) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiEncryptedIdentityEnvelopeError>
+pub fn rhi::reduce_rhi_reconciliation_manifest(rhi::RhiReconciliationManifest) -> core::result::Result<rhi::RhiReconciliationProjection, rhi::RhiReconciliationReducerError>
pub fn rhi::resolve_rhi_runtime_context(&radroots_runtime_paths::roots::RadrootsPathResolver, &rhi::RhiCliInvocationV1) -> core::result::Result<rhi::RhiRuntimeContext, rhi::RhiRuntimeContextError>
pub fn rhi::resolve_rhi_wrapping_credential(&rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError>
pub fn rhi::rhi_migration_catalog() -> core::result::Result<radroots_service_sqlite::migration::MigrationCatalog, rhi::RhiStateCatalogError>
diff --git a/contracts/services_hardening/reconciliation_manifest.v1.json b/contracts/services_hardening/reconciliation_manifest.v1.json
@@ -74,6 +74,17 @@
"debug": "counts_only_redacted",
"errors": "crate_owned_source_free_redacted"
},
+ "private_reducer_material": {
+ "construction": "deduplicated_from_confirmed_step_190_canonical_mutation_content",
+ "ordering": "mutation_id_ascending",
+ "representative_event": "smallest_event_id_for_mutation",
+ "maximum_mutations": 65536,
+ "maximum_canonical_content_bytes": 134217728,
+ "storage": "immutable_reference_counted_bytes",
+ "canonical_manifest_wire_changed": false,
+ "public_raw_access": false,
+ "reducer_owner": "reconciliation_reducer.v1.json"
+ },
"effects": {
"sqlite": false,
"filesystem": false,
@@ -94,7 +105,6 @@
],
"deferred": [
"wave_qualification",
- "lineage_reducer",
"coverage_and_outcome_projection",
"manifest_persistence",
"attestation",
diff --git a/contracts/services_hardening/reconciliation_reducer.v1.json b/contracts/services_hardening/reconciliation_reducer.v1.json
@@ -0,0 +1,81 @@
+{
+ "schema": "radroots.rhi.reconciliation-reducer",
+ "schema_version": 1,
+ "contract_version": 1,
+ "input": {
+ "authority": "sealed_confirmed_reconciliation_manifest_only",
+ "consumption": "owned_manifest_retained_inside_projection",
+ "mutation_material": "private_canonical_content_bound_to_manifest_observations",
+ "mutation_order": "mutation_id_ascending",
+ "representative_event": "smallest_event_id_for_mutation",
+ "maximum_mutations": 65536,
+ "maximum_canonical_content_bytes": 134217728,
+ "private_terms": "none_without_explicit_manifest_evidence",
+ "prior_attestations": "none_without_explicit_manifest_evidence",
+ "observed_time": "exact_manifest_observed_at_unix_seconds",
+ "evidence_state": {
+ "missing": "missing",
+ "partial": "query_partial",
+ "scope_satisfied": "complete",
+ "unsupported": "unsupported_version"
+ }
+ },
+ "shared_reducer": {
+ "contract_id": "radroots.trade.reducer.v1",
+ "contract_version": 1,
+ "canonical_projection": "radroots_trade_projection_v1",
+ "projection_digest": "shared_domain_separated_lowercase_hex_sha256"
+ },
+ "projection_digest": {
+ "algorithm": "sha256",
+ "domain_utf8_then_nul": "radroots.rhi.reconciliation_projection.v1",
+ "ordered_fields": [
+ "rhi_reducer_contract_version_u32_be",
+ "shared_reducer_contract_id_length_u64_be",
+ "shared_reducer_contract_id_utf8",
+ "shared_reducer_contract_version_u16_be",
+ "evidence_manifest_digest",
+ "evidence_policy_digest",
+ "shared_projection_digest"
+ ]
+ },
+ "result": {
+ "sealed": true,
+ "caller_forgeable": false,
+ "retains_manifest": true,
+ "shared_projection_public_access": false,
+ "shared_projection_retained_for_later_steps": true,
+ "shared_projection_digest_available": true,
+ "rhi_projection_digest_available": true,
+ "debug": "counts_only_redacted",
+ "errors": "crate_owned_source_free_redacted"
+ },
+ "effects": {
+ "sqlite": false,
+ "filesystem": false,
+ "source_or_relay": false,
+ "network": false,
+ "task_spawn": false,
+ "ambient_clock": false,
+ "ambient_entropy": false
+ },
+ "forbidden": [
+ "raw_manifest_constructor",
+ "caller_supplied_mutation_material",
+ "database_lookup",
+ "source_or_relay_lookup",
+ "arrival_order_selected_truth",
+ "unbound_projection_digest",
+ "final_coverage_or_outcome_authority",
+ "projection_persistence",
+ "report_or_attestation_construction"
+ ],
+ "deferred": [
+ "final_coverage_and_outcome",
+ "generation_fenced_commit",
+ "report_and_attestation",
+ "manifest_and_projection_persistence",
+ "publication",
+ "job_finalization"
+ ]
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -12,6 +12,7 @@ mod reconciliation_attempt;
mod reconciliation_commit;
mod reconciliation_job;
mod reconciliation_manifest;
+mod reconciliation_reducer;
mod reconciliation_replay;
mod runtime_adapters;
mod runtime_context;
@@ -94,6 +95,11 @@ pub use reconciliation_manifest::{
RhiReconciliationManifestError, RhiReconciliationManifestErrorKind,
RhiReconciliationScopePrerequisites,
};
+pub use reconciliation_reducer::{
+ RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION, RhiReconciliationProjection,
+ RhiReconciliationReducerError, RhiReconciliationReducerErrorKind,
+ reduce_rhi_reconciliation_manifest,
+};
pub use reconciliation_replay::{
RHI_RECONCILIATION_REPLAY_CONTRACT_VERSION, RhiReconciliationReplayError,
RhiReconciliationReplayErrorKind, RhiReconciliationSourceCursorEvidence,
diff --git a/src/reconciliation_manifest.rs b/src/reconciliation_manifest.rs
@@ -1,7 +1,7 @@
//! Immutable manifest materialization from one confirmed reconciliation commit.
use core::{fmt, num::NonZeroU64};
-use std::error::Error;
+use std::{collections::BTreeMap, error::Error, sync::Arc};
use radroots_event::id::{EventId, MutationId, TradeId};
use radroots_service_host::UnixTimeSeconds;
@@ -30,6 +30,8 @@ const SOURCE_RESULT_DIGEST_DOMAIN: &[u8] =
b"radroots.rhi.reconciliation_manifest_source_result.v1\0";
const PROVENANCE_DIGEST_DOMAIN: &[u8] = b"radroots.rhi.evidence_provenance.v1\0";
const SOURCE_SELECTOR: &[u8] = b"trade_mutation_lineage_v1";
+pub(crate) const RHI_REDUCER_MAXIMUM_MUTATIONS: usize = 65_536;
+pub(crate) const RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES: usize = 134_217_728;
/// Exact non-source prerequisite state bound into one manifest.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -111,6 +113,7 @@ impl Error for RhiReconciliationManifestError {}
/// ```
pub struct RhiReconciliationManifest {
inner: RadrootsTradeEvidenceManifestV1,
+ reducer_mutations: Box<[RhiReducerMutationMaterial]>,
}
impl RhiReconciliationManifest {
@@ -173,6 +176,14 @@ impl RhiReconciliationManifest {
pub fn observation_count(&self) -> usize {
self.inner.observations().len()
}
+
+ pub(crate) const fn inner(&self) -> &RadrootsTradeEvidenceManifestV1 {
+ &self.inner
+ }
+
+ pub(crate) fn reducer_mutations(&self) -> &[RhiReducerMutationMaterial] {
+ &self.reducer_mutations
+ }
}
impl fmt::Debug for RhiReconciliationManifest {
@@ -207,6 +218,13 @@ pub(crate) struct RhiCommittedManifestMaterial {
latest_finished_unix_ms: u64,
sources: Box<[RadrootsTradeEvidenceManifestSourceResultV1]>,
observations: Box<[RadrootsTradeEvidenceManifestObservationV1]>,
+ reducer_mutations: Box<[RhiReducerMutationMaterial]>,
+}
+
+pub(crate) struct RhiReducerMutationMaterial {
+ pub(crate) mutation_id: MutationId,
+ pub(crate) event_id: EventId,
+ pub(crate) canonical_content: Arc<[u8]>,
}
pub(crate) fn committed_manifest_material(
@@ -223,6 +241,7 @@ pub(crate) fn committed_manifest_material(
total.checked_add(part.facts.len()).ok_or(())
})?;
let mut observations = Vec::with_capacity(observation_capacity);
+ let mut reducer_mutations = BTreeMap::<[u8; 32], RhiReducerMutationMaterial>::new();
for (ordinal, part) in parts.iter().enumerate() {
if part.trade_id != trade_id
@@ -252,8 +271,31 @@ pub(crate) fn committed_manifest_material(
));
for fact in &part.facts {
observations.push(manifest_observation(source_id.clone(), part, fact)?);
+ reducer_mutations
+ .entry(fact.record.mutation_id)
+ .and_modify(|current| {
+ if fact.record.event_id < *current.event_id.as_bytes() {
+ current.event_id = EventId::from_bytes(fact.record.event_id);
+ current.canonical_content = fact.record.canonical_content.clone();
+ }
+ })
+ .or_insert_with(|| RhiReducerMutationMaterial {
+ mutation_id: MutationId::from_bytes(fact.record.mutation_id),
+ event_id: EventId::from_bytes(fact.record.event_id),
+ canonical_content: fact.record.canonical_content.clone(),
+ });
}
}
+ if reducer_mutations.len() > RHI_REDUCER_MAXIMUM_MUTATIONS
+ || reducer_mutations
+ .values()
+ .try_fold(0_usize, |total, material| {
+ total.checked_add(material.canonical_content.len())
+ })
+ .is_none_or(|total| total > RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES)
+ {
+ return Err(());
+ }
Ok(RhiCommittedManifestMaterial {
trade_id,
@@ -262,6 +304,10 @@ pub(crate) fn committed_manifest_material(
latest_finished_unix_ms,
sources: sources.into_boxed_slice(),
observations: observations.into_boxed_slice(),
+ reducer_mutations: reducer_mutations
+ .into_values()
+ .collect::<Vec<_>>()
+ .into_boxed_slice(),
})
}
@@ -293,7 +339,10 @@ fn freeze_manifest(
material.observations.into_vec(),
)
.map_err(|_| error(RhiReconciliationManifestErrorKind::InvalidCommittedInventory))?;
- Ok(RhiReconciliationManifest { inner })
+ Ok(RhiReconciliationManifest {
+ inner,
+ reducer_mutations: material.reducer_mutations,
+ })
}
fn map_completion(value: RhiTradeSourceCompletion) -> RadrootsTradeEvidenceSourceCompletionV1 {
diff --git a/src/reconciliation_reducer.rs b/src/reconciliation_reducer.rs
@@ -0,0 +1,358 @@
+//! Pure deterministic reduction of one sealed reconciliation manifest.
+
+use core::fmt;
+use std::{collections::BTreeSet, error::Error};
+
+use radroots_event::{
+ id::{MutationId, TradeId},
+ trade::trade_mutation_from_canonical_content,
+};
+use radroots_trade::{
+ evidence::{
+ RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceStateV1,
+ RadrootsTradeMutationRecordV1,
+ },
+ model::RadrootsTradeProjectionV1,
+ reducer::{
+ RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
+ RadrootsTradeReductionInputV1, reduce_trade_records,
+ },
+};
+use sha2::{Digest, Sha256};
+
+use crate::{
+ RhiReconciliationManifest,
+ reconciliation_manifest::{
+ RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES, RHI_REDUCER_MAXIMUM_MUTATIONS,
+ RhiReducerMutationMaterial,
+ },
+};
+
+/// Exact version of the RHI reconciliation-reducer binding.
+pub const RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION: u32 = 1;
+
+const PROJECTION_DIGEST_DOMAIN: &[u8] = b"radroots.rhi.reconciliation_projection.v1\0";
+
+/// Stable source-free reconciliation-reducer failure class.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiReconciliationReducerErrorKind {
+ InvalidManifest,
+ ProjectionUnavailable,
+}
+
+impl RhiReconciliationReducerErrorKind {
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidManifest => "reconciliation_reducer_manifest_invalid",
+ Self::ProjectionUnavailable => "reconciliation_reducer_projection_unavailable",
+ }
+ }
+}
+
+/// Redacted source-free reconciliation-reducer failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiReconciliationReducerError {
+ kind: RhiReconciliationReducerErrorKind,
+}
+
+impl RhiReconciliationReducerError {
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiReconciliationReducerErrorKind {
+ 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 RhiReconciliationReducerError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiReconciliationReducerErrorKind::InvalidManifest => {
+ "RHI reconciliation manifest cannot be reduced"
+ }
+ RhiReconciliationReducerErrorKind::ProjectionUnavailable => {
+ "RHI reconciliation projection is unavailable"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiReconciliationReducerError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiReconciliationReducerError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiReconciliationReducerError {}
+
+/// Sealed deterministic projection retaining its exact manifest capability.
+///
+/// Callers cannot construct or relabel a projection.
+///
+/// ```compile_fail
+/// use rhi::RhiReconciliationProjection;
+///
+/// let _forged = RhiReconciliationProjection {};
+/// ```
+pub struct RhiReconciliationProjection {
+ manifest: RhiReconciliationManifest,
+ shared: RadrootsTradeProjectionV1,
+ shared_projection_digest: [u8; 32],
+ digest: [u8; 32],
+}
+
+impl RhiReconciliationProjection {
+ /// Returns the exact RHI reducer-binding contract version.
+ #[must_use]
+ pub const fn contract_version(&self) -> u32 {
+ RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION
+ }
+
+ /// Returns the exact shared reducer contract ID.
+ #[must_use]
+ pub const fn shared_reducer_contract_id(&self) -> &'static str {
+ RADROOTS_TRADE_REDUCER_CONTRACT_ID
+ }
+
+ /// Returns the exact shared reducer contract version.
+ #[must_use]
+ pub const fn shared_reducer_contract_version(&self) -> u16 {
+ RADROOTS_TRADE_REDUCER_VERSION
+ }
+
+ /// Returns the sealed manifest consumed by this projection.
+ #[must_use]
+ pub const fn manifest(&self) -> &RhiReconciliationManifest {
+ &self.manifest
+ }
+
+ /// Returns the exact trade selected by the immutable manifest.
+ #[must_use]
+ pub const fn trade_id(&self) -> &TradeId {
+ self.manifest.trade_id()
+ }
+
+ /// Returns the exact shared projection digest decoded from lowercase hex.
+ #[must_use]
+ pub const fn shared_projection_digest(&self) -> [u8; 32] {
+ self.shared_projection_digest
+ }
+
+ /// Returns the domain-separated RHI projection digest.
+ #[must_use]
+ pub const fn digest(&self) -> [u8; 32] {
+ self.digest
+ }
+
+ /// Returns the number of canonical shared reducer issues.
+ #[must_use]
+ pub fn issue_count(&self) -> usize {
+ self.shared.issues().len()
+ }
+
+ /// Returns the selected root mutation when the manifest establishes one.
+ #[must_use]
+ pub const fn root_mutation_id(&self) -> Option<&MutationId> {
+ self.shared.root_mutation_id()
+ }
+}
+
+impl fmt::Debug for RhiReconciliationProjection {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiReconciliationProjection")
+ .field("source_count", &self.manifest.source_count())
+ .field("observation_count", &self.manifest.observation_count())
+ .field("issue_count", &self.issue_count())
+ .finish_non_exhaustive()
+ }
+}
+
+/// Reduces one sealed immutable reconciliation manifest without external I/O.
+pub fn reduce_rhi_reconciliation_manifest(
+ manifest: RhiReconciliationManifest,
+) -> Result<RhiReconciliationProjection, RhiReconciliationReducerError> {
+ let inner = manifest.inner();
+ if !mutation_material_within_bounds(manifest.reducer_mutations()) {
+ return Err(failure(RhiReconciliationReducerErrorKind::InvalidManifest));
+ }
+ let observation_inventory = inner
+ .observations()
+ .iter()
+ .map(|observation| (*observation.mutation_id(), *observation.event_id()))
+ .collect::<BTreeSet<_>>();
+ if inner.observations().iter().any(|observation| {
+ manifest
+ .reducer_mutations()
+ .binary_search_by_key(observation.mutation_id(), |material| material.mutation_id)
+ .is_err()
+ }) {
+ return Err(failure(RhiReconciliationReducerErrorKind::InvalidManifest));
+ }
+
+ let mut mutations = Vec::with_capacity(manifest.reducer_mutations().len());
+ for material in manifest.reducer_mutations() {
+ if !observation_inventory.contains(&(material.mutation_id, material.event_id)) {
+ return Err(failure(RhiReconciliationReducerErrorKind::InvalidManifest));
+ }
+ let content = core::str::from_utf8(&material.canonical_content)
+ .map_err(|_| failure(RhiReconciliationReducerErrorKind::InvalidManifest))?;
+ let mutation = trade_mutation_from_canonical_content(content)
+ .map_err(|_| failure(RhiReconciliationReducerErrorKind::InvalidManifest))?;
+ if mutation.mutation_id != Some(material.mutation_id)
+ || mutation.trade_id != *inner.trade_id()
+ {
+ return Err(failure(RhiReconciliationReducerErrorKind::InvalidManifest));
+ }
+ mutations.push(RadrootsTradeMutationRecordV1::new(
+ Some(material.event_id),
+ mutation,
+ ));
+ }
+
+ let input = RadrootsTradeReductionInputV1::new(*inner.trade_id())
+ .with_mutations(mutations)
+ .with_evidence_state(evidence_state(inner.coverage()))
+ .with_observed_at_unix_s(Some(inner.observed_at_unix_s()));
+ let shared = reduce_trade_records(input);
+ if shared.reducer_contract_id() != RADROOTS_TRADE_REDUCER_CONTRACT_ID
+ || shared.reducer_version() != RADROOTS_TRADE_REDUCER_VERSION
+ || shared.trade_id() != inner.trade_id()
+ {
+ return Err(failure(
+ RhiReconciliationReducerErrorKind::ProjectionUnavailable,
+ ));
+ }
+ let shared_projection_digest = decode_lower_hex_32(shared.projection_digest())
+ .ok_or_else(|| failure(RhiReconciliationReducerErrorKind::ProjectionUnavailable))?;
+ let digest = projection_digest(&manifest, shared_projection_digest)
+ .ok_or_else(|| failure(RhiReconciliationReducerErrorKind::ProjectionUnavailable))?;
+ Ok(RhiReconciliationProjection {
+ manifest,
+ shared,
+ shared_projection_digest,
+ digest,
+ })
+}
+
+fn mutation_material_within_bounds(materials: &[RhiReducerMutationMaterial]) -> bool {
+ material_lengths_within_bounds(
+ materials.len(),
+ materials
+ .iter()
+ .map(|material| material.canonical_content.len()),
+ )
+}
+
+fn material_lengths_within_bounds<I>(count: usize, lengths: I) -> bool
+where
+ I: IntoIterator<Item = usize>,
+{
+ count <= RHI_REDUCER_MAXIMUM_MUTATIONS
+ && lengths
+ .into_iter()
+ .try_fold(0_usize, usize::checked_add)
+ .is_some_and(|total| total <= RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES)
+}
+
+fn evidence_state(coverage: RadrootsTradeEvidenceCoverageV1) -> RadrootsTradeEvidenceStateV1 {
+ match coverage {
+ RadrootsTradeEvidenceCoverageV1::Missing => RadrootsTradeEvidenceStateV1::Missing,
+ RadrootsTradeEvidenceCoverageV1::Partial => RadrootsTradeEvidenceStateV1::QueryPartial,
+ RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => RadrootsTradeEvidenceStateV1::Complete,
+ RadrootsTradeEvidenceCoverageV1::Unsupported => {
+ RadrootsTradeEvidenceStateV1::UnsupportedVersion
+ }
+ }
+}
+
+fn projection_digest(manifest: &RhiReconciliationManifest, shared: [u8; 32]) -> Option<[u8; 32]> {
+ let inner = manifest.inner();
+ let mut digest = Sha256::new();
+ digest.update(PROJECTION_DIGEST_DOMAIN);
+ digest.update(RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION.to_be_bytes());
+ digest.update(
+ u64::try_from(RADROOTS_TRADE_REDUCER_CONTRACT_ID.len())
+ .ok()?
+ .to_be_bytes(),
+ );
+ digest.update(RADROOTS_TRADE_REDUCER_CONTRACT_ID.as_bytes());
+ digest.update(RADROOTS_TRADE_REDUCER_VERSION.to_be_bytes());
+ digest.update(manifest.digest());
+ digest.update(inner.evidence_policy_digest().as_bytes());
+ digest.update(shared);
+ Some(digest.finalize().into())
+}
+
+fn decode_lower_hex_32(value: &str) -> Option<[u8; 32]> {
+ if value.len() != 64 {
+ return None;
+ }
+ let mut output = [0_u8; 32];
+ for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
+ output[index] = decode_lower_hex(pair[0])?
+ .checked_mul(16)?
+ .checked_add(decode_lower_hex(pair[1])?)?;
+ }
+ Some(output)
+}
+
+const fn decode_lower_hex(value: u8) -> Option<u8> {
+ match value {
+ b'0'..=b'9' => Some(value - b'0'),
+ b'a'..=b'f' => Some(value - b'a' + 10),
+ _ => None,
+ }
+}
+
+const fn failure(kind: RhiReconciliationReducerErrorKind) -> RhiReconciliationReducerError {
+ RhiReconciliationReducerError { kind }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn diagnostics_and_digest_decoder_are_closed() {
+ for kind in [
+ RhiReconciliationReducerErrorKind::InvalidManifest,
+ RhiReconciliationReducerErrorKind::ProjectionUnavailable,
+ ] {
+ let error = failure(kind);
+ assert_eq!(error.kind(), kind);
+ assert!(Error::source(&error).is_none());
+ assert!(!format!("{error} {error:?}").contains("trade-primary"));
+ }
+ assert_eq!(decode_lower_hex_32(&"ab".repeat(32)), Some([0xab; 32]));
+ assert_eq!(decode_lower_hex_32(&"AB".repeat(32)), None);
+ assert_eq!(decode_lower_hex_32("00"), None);
+ }
+
+ #[test]
+ fn mutation_material_length_and_count_bounds_are_exact() {
+ assert!(material_lengths_within_bounds(
+ RHI_REDUCER_MAXIMUM_MUTATIONS,
+ [RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES]
+ ));
+ assert!(!material_lengths_within_bounds(
+ RHI_REDUCER_MAXIMUM_MUTATIONS,
+ [RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES, 1]
+ ));
+ assert!(!material_lengths_within_bounds(
+ RHI_REDUCER_MAXIMUM_MUTATIONS + 1,
+ core::iter::empty()
+ ));
+ assert!(!material_lengths_within_bounds(1, [usize::MAX, 1]));
+ }
+}
diff --git a/src/state_trade.rs b/src/state_trade.rs
@@ -1,7 +1,7 @@
//! Atomic immutable persistence for admitted trade-event evidence.
use core::fmt;
-use std::error::Error;
+use std::{error::Error, sync::Arc};
use radroots_service_sqlite::{
ServiceSqliteTransaction, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind,
@@ -316,7 +316,7 @@ pub(crate) struct PersistenceRecord {
pub(crate) author_pubkey: [u8; 32],
pub(crate) event_kind: u32,
pub(crate) authored_at_unix_s: u64,
- pub(crate) canonical_content: Box<[u8]>,
+ pub(crate) canonical_content: Arc<[u8]>,
pub(crate) canonical_event_json: Box<[u8]>,
}
@@ -345,7 +345,7 @@ impl PersistenceRecord {
author_pubkey: *event.author().as_bytes(),
event_kind: event.kind_u32(),
authored_at_unix_s: event.created_at_u64(),
- canonical_content: canonical_content.into_boxed_slice(),
+ canonical_content: canonical_content.into(),
canonical_event_json: canonical_event_json.into_boxed_slice(),
})
}
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -14,6 +14,7 @@ const RUNTIME_FOUNDATION: &str = include_str!("../src/runtime_foundation.rs");
const RECONCILIATION_ATTEMPTS: &str = include_str!("../src/reconciliation_attempt.rs");
const RECONCILIATION_COMMIT: &str = include_str!("../src/reconciliation_commit.rs");
const RECONCILIATION_MANIFEST: &str = include_str!("../src/reconciliation_manifest.rs");
+const RECONCILIATION_REDUCER: &str = include_str!("../src/reconciliation_reducer.rs");
const RECONCILIATION_JOBS: &str = include_str!("../src/reconciliation_job.rs");
const RECONCILIATION_REPLAY: &str = include_str!("../src/reconciliation_replay.rs");
const RECONCILIATION_ATTEMPT_CONTRACT: &str =
@@ -24,6 +25,8 @@ const RECONCILIATION_COMMIT_CONTRACT: &str =
include_str!("../contracts/services_hardening/reconciliation_commit.v1.json");
const RECONCILIATION_MANIFEST_CONTRACT: &str =
include_str!("../contracts/services_hardening/reconciliation_manifest.v1.json");
+const RECONCILIATION_REDUCER_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/reconciliation_reducer.v1.json");
const RUNTIME_FOUNDATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/runtime_foundation.v1.json");
const TRADE_INGEST_CONTRACT: &str =
@@ -44,6 +47,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/reconciliation_commit.rs"),
include_str!("../src/reconciliation_job.rs"),
include_str!("../src/reconciliation_manifest.rs"),
+ include_str!("../src/reconciliation_reducer.rs"),
include_str!("../src/reconciliation_replay.rs"),
include_str!("../src/runtime_context.rs"),
include_str!("../src/runtime_adapters.rs"),
@@ -115,6 +119,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"reconciliation_commit",
"reconciliation_job",
"reconciliation_manifest",
+ "reconciliation_reducer",
"reconciliation_replay",
"runtime_context",
"runtime_adapters",
@@ -160,6 +165,9 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"RhiReconciliationManifest",
"RhiReconciliationManifestErrorKind",
"RhiReconciliationScopePrerequisites",
+ "RhiReconciliationProjection",
+ "RhiReconciliationReducerErrorKind",
+ "reduce_rhi_reconciliation_manifest",
"RhiReconciliationSourceReplayPlan",
"RhiReconciliationSourceReplay",
"RhiReconciliationJobPolicy",
@@ -278,6 +286,43 @@ fn reconciliation_manifest_is_canonical_sealed_and_effect_free() {
}
#[test]
+fn reconciliation_reducer_is_manifest_bound_sealed_and_effect_free() {
+ let contract: serde_json::Value = serde_json::from_str(RECONCILIATION_REDUCER_CONTRACT)
+ .expect("reconciliation-reducer contract");
+ assert_eq!(contract["schema"], "radroots.rhi.reconciliation-reducer");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["input"]["maximum_mutations"], 65_536);
+ assert_eq!(
+ contract["input"]["maximum_canonical_content_bytes"],
+ 134_217_728
+ );
+ assert_eq!(contract["effects"]["sqlite"], false);
+ for required in [
+ "trade_mutation_from_canonical_content",
+ "reduce_trade_records(input)",
+ "PROJECTION_DIGEST_DOMAIN",
+ "manifest.digest()",
+ "inner.evidence_policy_digest()",
+ "pub fn reduce_rhi_reconciliation_manifest(",
+ "RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES",
+ ] {
+ assert!(
+ RECONCILIATION_REDUCER.contains(required),
+ "reconciliation reducer is missing {required}"
+ );
+ }
+ for forbidden in ["sqlx::", "std::fs", "std::net", "tokio::", "SystemTime"] {
+ assert!(
+ !RECONCILIATION_REDUCER.contains(forbidden),
+ "reconciliation reducer gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(!ROOT.contains("pub mod reconciliation_reducer"));
+ assert!(!PUBLIC_API.contains("rhi::reconciliation_reducer::"));
+ assert!(!PUBLIC_API.contains("RhiReconciliationProjection::shared_projection(&self)"));
+}
+
+#[test]
fn public_errors_are_crate_owned_redacted_and_source_free() {
let production = SOURCES.join("\n");
assert!(!production.contains("fn source("));
@@ -309,7 +354,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, 21);
+ assert_eq!(public_error_count, 22);
}
#[test]
@@ -729,6 +774,10 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"[`reconciliation_commit.v1.json`](contracts/services_hardening/reconciliation_commit.v1.json)",
"## Immutable reconciliation manifest",
"[`reconciliation_manifest.v1.json`](contracts/services_hardening/reconciliation_manifest.v1.json)",
+ "## Pure reconciliation reducer",
+ "[`reconciliation_reducer.v1.json`](contracts/services_hardening/reconciliation_reducer.v1.json)",
+ "binds the promoted shared `radroots.trade.reducer.v1`",
+ "Final four-state coverage, three-state outcome",
"The Step 192 integration-wave qualification proves that concurrent exact",
"lost-success retry converges after close/reopen",
"inventory terminates at configured source count plus one before mutation",
diff --git a/tests/services_hardening_reconciliation_jobs.rs b/tests/services_hardening_reconciliation_jobs.rs
@@ -17,7 +17,7 @@ use rhi::{
RhiTradeMutationObservedAtUnixSeconds, RhiTradeSourceCompletion, TradeId, UnixTimeSeconds,
admit_rhi_trade_mutation_event, 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,
+ reduce_rhi_reconciliation_manifest, resolve_rhi_runtime_context,
};
use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions};
@@ -1052,6 +1052,33 @@ async fn source_replay_commit_is_atomic_idempotent_and_mints_durable_cursor_evid
]
);
let canonical_manifest = manifest.canonical_bytes().to_vec();
+ let projection = reduce_rhi_reconciliation_manifest(manifest).expect("pure projection");
+ assert_eq!(projection.contract_version(), 1);
+ assert_eq!(
+ projection.shared_reducer_contract_id(),
+ "radroots.trade.reducer.v1"
+ );
+ assert_eq!(projection.shared_reducer_contract_version(), 1);
+ assert_eq!(projection.trade_id(), &TradeId::from_bytes([0x11; 16]));
+ assert_eq!(projection.manifest().canonical_bytes(), canonical_manifest);
+ assert!(projection.root_mutation_id().is_some());
+ assert_eq!(projection.issue_count(), 0);
+ assert_eq!(
+ projection.shared_projection_digest(),
+ [
+ 0x21, 0xd5, 0xd5, 0xe6, 0x06, 0x7a, 0x13, 0x68, 0xd0, 0xd5, 0x25, 0xa3, 0xec, 0xd1,
+ 0xb5, 0xcc, 0x99, 0xcb, 0x03, 0xd7, 0xf8, 0x06, 0xe6, 0xba, 0x47, 0xd3, 0xb9, 0x29,
+ 0x99, 0xa7, 0xe9, 0x61,
+ ]
+ );
+ assert_eq!(
+ projection.digest(),
+ [
+ 0xd1, 0x33, 0xa7, 0x72, 0xd2, 0x87, 0xa2, 0x56, 0x4a, 0xb3, 0xb3, 0xb2, 0xca, 0xb6,
+ 0xdc, 0xa6, 0xe5, 0xc5, 0xa0, 0x7f, 0x30, 0x8f, 0x67, 0xc5, 0xee, 0x77, 0x38, 0x06,
+ 0x40, 0x7a, 0x03, 0x71,
+ ]
+ );
host.close()
.await
.expect("close before lost-success replay");
diff --git a/tests/services_hardening_reconciliation_manifest_contract.rs b/tests/services_hardening_reconciliation_manifest_contract.rs
@@ -66,6 +66,24 @@ fn machine_contract_freezes_the_complete_step_191_boundary() {
assert_eq!(contract["effects"]["sqlite"], false);
assert_eq!(contract["effects"]["ambient_clock"], false);
assert_eq!(contract["effects"]["ambient_entropy"], false);
+ assert_eq!(
+ contract["private_reducer_material"]["construction"],
+ "deduplicated_from_confirmed_step_190_canonical_mutation_content"
+ );
+ assert_eq!(
+ contract["private_reducer_material"]["canonical_manifest_wire_changed"],
+ false
+ );
+ assert_eq!(
+ contract["private_reducer_material"]["maximum_canonical_content_bytes"],
+ 134_217_728
+ );
+ assert!(
+ !contract["deferred"]
+ .as_array()
+ .expect("deferred inventory")
+ .contains(&json!("lineage_reducer"))
+ );
}
#[test]
diff --git a/tests/services_hardening_reconciliation_reducer_contract.rs b/tests/services_hardening_reconciliation_reducer_contract.rs
@@ -0,0 +1,120 @@
+#![forbid(unsafe_code)]
+
+use rhi::RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION;
+use serde_json::json;
+
+const CONTRACT: &str =
+ include_str!("../contracts/services_hardening/reconciliation_reducer.v1.json");
+const ROOT: &str = include_str!("../src/lib.rs");
+const SOURCE: &str = include_str!("../src/reconciliation_reducer.rs");
+const MANIFEST: &str = include_str!("../src/reconciliation_manifest.rs");
+const README: &str = include_str!("../README");
+
+#[test]
+fn machine_contract_freezes_the_complete_step_193_boundary() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
+ assert_eq!(contract["schema"], "radroots.rhi.reconciliation-reducer");
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(
+ contract["contract_version"],
+ RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION
+ );
+ assert_eq!(
+ contract["input"]["authority"],
+ "sealed_confirmed_reconciliation_manifest_only"
+ );
+ assert_eq!(contract["input"]["maximum_mutations"], 65_536);
+ assert_eq!(
+ contract["input"]["maximum_canonical_content_bytes"],
+ 134_217_728
+ );
+ assert_eq!(
+ contract["input"]["evidence_state"],
+ json!({
+ "missing": "missing",
+ "partial": "query_partial",
+ "scope_satisfied": "complete",
+ "unsupported": "unsupported_version"
+ })
+ );
+ assert_eq!(
+ contract["shared_reducer"]["contract_id"],
+ "radroots.trade.reducer.v1"
+ );
+ assert_eq!(
+ contract["projection_digest"]["ordered_fields"],
+ json!([
+ "rhi_reducer_contract_version_u32_be",
+ "shared_reducer_contract_id_length_u64_be",
+ "shared_reducer_contract_id_utf8",
+ "shared_reducer_contract_version_u16_be",
+ "evidence_manifest_digest",
+ "evidence_policy_digest",
+ "shared_projection_digest"
+ ])
+ );
+ assert_eq!(contract["effects"]["sqlite"], false);
+ assert_eq!(contract["effects"]["source_or_relay"], false);
+ assert_eq!(contract["effects"]["ambient_clock"], false);
+ assert_eq!(contract["effects"]["ambient_entropy"], false);
+}
+
+#[test]
+fn reducer_boundary_is_sealed_redacted_and_effect_free() {
+ assert!(ROOT.contains("mod reconciliation_reducer;"));
+ assert!(!ROOT.contains("pub mod reconciliation_reducer;"));
+ for required in [
+ "RhiReconciliationProjection",
+ "RhiReconciliationReducerError",
+ "RhiReconciliationReducerErrorKind",
+ "RHI_RECONCILIATION_REDUCER_CONTRACT_VERSION",
+ "reduce_rhi_reconciliation_manifest",
+ ] {
+ assert!(ROOT.contains(required), "root API is missing {required}");
+ }
+ for required in [
+ "trade_mutation_from_canonical_content",
+ "reduce_trade_records(input)",
+ "PROJECTION_DIGEST_DOMAIN",
+ "manifest.digest()",
+ "inner.evidence_policy_digest()",
+ "decode_lower_hex_32(shared.projection_digest())",
+ "RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES",
+ ] {
+ assert!(SOURCE.contains(required), "reducer is missing {required}");
+ }
+ assert!(!SOURCE.contains("fn shared_projection(&self)"));
+ for required in [
+ "reducer_mutations: Box<[RhiReducerMutationMaterial]>",
+ "BTreeMap::<[u8; 32], RhiReducerMutationMaterial>::new()",
+ "RHI_REDUCER_MAXIMUM_MUTATION_MATERIAL_BYTES: usize = 134_217_728",
+ "canonical_content: Arc<[u8]>",
+ "canonical_content: fact.record.canonical_content.clone()",
+ ] {
+ assert!(
+ MANIFEST.contains(required),
+ "manifest reducer material is missing {required}"
+ );
+ }
+ for forbidden in [
+ "sqlx::",
+ "std::fs",
+ "std::net",
+ "tokio::",
+ "SystemTime",
+ "thread_rng",
+ "OsRng",
+ "pub fn new(",
+ "pub const fn new(",
+ "pub fn persist",
+ ] {
+ assert!(
+ !SOURCE.contains(forbidden),
+ "reducer gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(README.contains("## Pure reconciliation reducer"));
+ assert!(README.contains(
+ "[`reconciliation_reducer.v1.json`](contracts/services_hardening/reconciliation_reducer.v1.json)"
+ ));
+}