commit a8c12c9cf38550a682f75a507ce18bf5c193bb45
parent 60ff7b07043de548ae5c398285ba2c155d2e9704
Author: triesap <tyson@radroots.org>
Date: Wed, 1 Jul 2026 11:08:40 +0000
rhi: gate deterministic validation to repo-local mode
- require explicit repo-local runtime policy for deterministic_none
- reject deterministic_none under production-capable runtime profiles
- update deterministic worker tests to use the explicit non-production policy
- guard against reintroducing ambiguous deterministic prover configuration
Diffstat:
3 files changed, 241 insertions(+), 9 deletions(-)
diff --git a/src/config.rs b/src/config.rs
@@ -4,7 +4,10 @@ use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
-use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy;
+use crate::features::trade_validation_receipt::{
+ TradeValidationReceiptProverBackend, TradeValidationReceiptProverPolicy,
+ TradeValidationReceiptRuntimePolicy,
+};
use crate::paths::{
RhiRuntimePaths, default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
};
@@ -219,6 +222,34 @@ impl RawSettings {
}
}
+fn validate_trade_validation_receipt_runtime_profile(
+ policy: &TradeValidationReceiptProverPolicy,
+ profile: radroots_runtime_paths::RadrootsPathProfile,
+) -> Result<()> {
+ if policy.runtime_policy == TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment
+ && policy.backend != TradeValidationReceiptProverBackend::DeterministicNone
+ {
+ bail!(
+ "trade_validation_receipt.runtime_policy repo_local_development requires backend deterministic_none"
+ );
+ }
+ if policy.backend == TradeValidationReceiptProverBackend::DeterministicNone
+ && policy.runtime_policy != TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment
+ {
+ bail!(
+ "trade_validation_receipt.backend deterministic_none requires runtime_policy repo_local_development"
+ );
+ }
+ if policy.backend == TradeValidationReceiptProverBackend::DeterministicNone
+ && profile != radroots_runtime_paths::RadrootsPathProfile::RepoLocal
+ {
+ bail!(
+ "trade_validation_receipt.backend deterministic_none requires runtime profile repo_local"
+ );
+ }
+ Ok(())
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub metadata: RadrootsNostrMetadata,
@@ -236,7 +267,12 @@ fn load_settings_from_path_with_resolver(
.with_context(|| format!("read configuration from {}", path.display()))?;
let settings: RawSettings =
toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?;
- settings.into_settings(&paths)
+ let settings = settings.into_settings(&paths)?;
+ validate_trade_validation_receipt_runtime_profile(
+ &settings.config.trade_validation_receipt,
+ profile,
+ )?;
+ Ok(settings)
}
pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
@@ -252,7 +288,9 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
#[cfg(test)]
mod tests {
use super::load_settings_from_path_with_resolver;
- use crate::features::trade_validation_receipt::TradeValidationReceiptProverBackend;
+ use crate::features::trade_validation_receipt::{
+ TradeValidationReceiptProverBackend, TradeValidationReceiptRuntimePolicy,
+ };
use crate::paths::{
default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
runtime_contract_with_resolver,
@@ -480,15 +518,17 @@ path = "state/trade-listing.json"
[trade_validation_receipt]
backend = "deterministic_none"
proof_mode = "none"
+runtime_policy = "repo_local_development"
"#,
)
.expect("write config");
+ let repo_local_root = temp.path().join("repo-local-runtime");
let settings = load_settings_from_path_with_resolver(
&config_path,
&linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
+ RadrootsPathProfile::RepoLocal,
+ Some(repo_local_root.as_path()),
)
.expect("load settings");
@@ -527,6 +567,83 @@ proof_mode = "none"
settings.config.trade_validation_receipt.proof_mode,
RadrootsSp1TradeProofMode::None
);
+ assert_eq!(
+ settings.config.trade_validation_receipt.runtime_policy,
+ TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment
+ );
+ }
+
+ #[test]
+ fn load_settings_rejects_deterministic_policy_without_repo_local_runtime_policy() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let config_path = temp.path().join("config.toml");
+ std::fs::write(
+ &config_path,
+ r#"
+[metadata]
+name = "rhi-test"
+
+[trade_validation_receipt]
+backend = "deterministic_none"
+proof_mode = "none"
+"#,
+ )
+ .expect("write config");
+
+ let repo_local_root = temp.path().join("repo-local-runtime");
+ let error = load_settings_from_path_with_resolver(
+ &config_path,
+ &linux_resolver(),
+ RadrootsPathProfile::RepoLocal,
+ Some(repo_local_root.as_path()),
+ )
+ .expect_err("deterministic_none without runtime policy must fail");
+ let message = format!("{error:#}");
+ assert!(
+ message.contains(
+ "trade_validation_receipt.backend deterministic_none requires runtime_policy repo_local_development"
+ ),
+ "{message}"
+ );
+ }
+
+ #[test]
+ fn load_settings_rejects_deterministic_policy_outside_repo_local_profile() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let config_path = temp.path().join("config.toml");
+ std::fs::write(
+ &config_path,
+ r#"
+[metadata]
+name = "rhi-test"
+
+[trade_validation_receipt]
+backend = "deterministic_none"
+proof_mode = "none"
+runtime_policy = "repo_local_development"
+"#,
+ )
+ .expect("write config");
+
+ for profile in [
+ RadrootsPathProfile::InteractiveUser,
+ RadrootsPathProfile::ServiceHost,
+ ] {
+ let error = load_settings_from_path_with_resolver(
+ &config_path,
+ &linux_resolver(),
+ profile,
+ None,
+ )
+ .expect_err("production-capable profiles must reject deterministic_none");
+ let message = format!("{error:#}");
+ assert!(
+ message.contains(
+ "trade_validation_receipt.backend deterministic_none requires runtime profile repo_local"
+ ),
+ "{message}"
+ );
+ }
}
#[test]
diff --git a/src/features/trade_validation_receipt.rs b/src/features/trade_validation_receipt.rs
@@ -101,12 +101,31 @@ impl TradeValidationReceiptProverBackend {
}
}
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeValidationReceiptRuntimePolicy {
+ #[default]
+ Production,
+ RepoLocalDevelopment,
+}
+
+impl TradeValidationReceiptRuntimePolicy {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Production => "production",
+ Self::RepoLocalDevelopment => "repo_local_development",
+ }
+ }
+}
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TradeValidationReceiptProverPolicy {
pub backend: TradeValidationReceiptProverBackend,
pub proof_mode: RadrootsSp1TradeProofMode,
#[serde(default)]
+ pub runtime_policy: TradeValidationReceiptRuntimePolicy,
+ #[serde(default)]
pub expected_sp1_program_hash: Option<String>,
#[serde(default)]
pub expected_sp1_verifying_key_hash: Option<String>,
@@ -125,16 +144,18 @@ impl TradeValidationReceiptProverPolicy {
Self {
backend: TradeValidationReceiptProverBackend::Disabled,
proof_mode: RadrootsSp1TradeProofMode::None,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: None,
expected_sp1_verifying_key_hash: None,
remote_http: None,
}
}
- pub fn deterministic_none() -> Self {
+ pub fn repo_local_deterministic_none() -> Self {
Self {
backend: TradeValidationReceiptProverBackend::DeterministicNone,
proof_mode: RadrootsSp1TradeProofMode::None,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment,
expected_sp1_program_hash: None,
expected_sp1_verifying_key_hash: None,
remote_http: None,
@@ -144,6 +165,13 @@ impl TradeValidationReceiptProverPolicy {
pub fn validate(&self) -> Result<(), TradeValidationReceiptJobError> {
validate_optional_hash32(&self.expected_sp1_program_hash)?;
validate_optional_hash32(&self.expected_sp1_verifying_key_hash)?;
+ if self.backend != TradeValidationReceiptProverBackend::DeterministicNone
+ && self.runtime_policy == TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment
+ {
+ return Err(
+ TradeValidationReceiptJobError::RepoLocalDevelopmentPolicyRequiresDeterministicNone,
+ );
+ }
match self.backend {
TradeValidationReceiptProverBackend::Disabled => {
if self.proof_mode != RadrootsSp1TradeProofMode::None {
@@ -160,6 +188,14 @@ impl TradeValidationReceiptProverPolicy {
}
TradeValidationReceiptProverBackend::DeterministicNone
| TradeValidationReceiptProverBackend::LocalExecute => {
+ if self.backend == TradeValidationReceiptProverBackend::DeterministicNone
+ && self.runtime_policy
+ != TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment
+ {
+ return Err(
+ TradeValidationReceiptJobError::DeterministicNoneRequiresRepoLocalDevelopment,
+ );
+ }
if self.proof_mode != RadrootsSp1TradeProofMode::None {
return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone);
}
@@ -435,6 +471,10 @@ pub enum TradeValidationReceiptJobError {
ProverBackendDisabled,
#[error("rhi prover backend requires proof_mode none")]
ProverBackendRequiresNone,
+ #[error("rhi deterministic_none prover backend requires repo_local_development runtime policy")]
+ DeterministicNoneRequiresRepoLocalDevelopment,
+ #[error("rhi repo_local_development runtime policy requires deterministic_none prover backend")]
+ RepoLocalDevelopmentPolicyRequiresDeterministicNone,
#[error("rhi prover backend requires an SP1 proof mode")]
ProverBackendRequiresSp1Proof,
#[error("rhi prover backend does not match configured policy")]
@@ -2467,8 +2507,8 @@ mod tests {
TradeValidationReceiptJobError, TradeValidationReceiptJobResult,
TradeValidationReceiptLocalWorkerRequest, TradeValidationReceiptProverBackend,
TradeValidationReceiptProverPolicy, TradeValidationReceiptRemoteHttpAuth,
- TradeValidationReceiptRemoteHttpProverConfig, TradeValidationReceiptTestHooks,
- build_trade_validation_receipt_job_request_event,
+ TradeValidationReceiptRemoteHttpProverConfig, TradeValidationReceiptRuntimePolicy,
+ TradeValidationReceiptTestHooks, build_trade_validation_receipt_job_request_event,
handle_trade_validation_receipt_job_request,
handle_trade_validation_receipt_local_worker_request, trade_validation_receipt_test_hooks,
};
@@ -2945,7 +2985,7 @@ mod tests {
}
fn deterministic_policy() -> TradeValidationReceiptProverPolicy {
- TradeValidationReceiptProverPolicy::deterministic_none()
+ TradeValidationReceiptProverPolicy::repo_local_deterministic_none()
}
fn hash32(ch: char) -> String {
@@ -2967,6 +3007,7 @@ mod tests {
TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
proof_mode: RadrootsSp1TradeProofMode::Core,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: Some(hash32('a')),
expected_sp1_verifying_key_hash: Some(hash32('b')),
remote_http: Some(remote_http_config()),
@@ -3195,10 +3236,44 @@ mod tests {
}
#[test]
+ fn deterministic_none_policy_requires_explicit_repo_local_runtime_policy() {
+ let policy = TradeValidationReceiptProverPolicy {
+ backend: TradeValidationReceiptProverBackend::DeterministicNone,
+ proof_mode: RadrootsSp1TradeProofMode::None,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
+ expected_sp1_program_hash: None,
+ expected_sp1_verifying_key_hash: None,
+ remote_http: None,
+ };
+ assert!(matches!(
+ policy.validate(),
+ Err(TradeValidationReceiptJobError::DeterministicNoneRequiresRepoLocalDevelopment)
+ ));
+
+ assert!(deterministic_policy().validate().is_ok());
+
+ let policy = TradeValidationReceiptProverPolicy {
+ backend: TradeValidationReceiptProverBackend::LocalExecute,
+ proof_mode: RadrootsSp1TradeProofMode::None,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::RepoLocalDevelopment,
+ expected_sp1_program_hash: None,
+ expected_sp1_verifying_key_hash: None,
+ remote_http: None,
+ };
+ assert!(matches!(
+ policy.validate(),
+ Err(
+ TradeValidationReceiptJobError::RepoLocalDevelopmentPolicyRequiresDeterministicNone
+ )
+ ));
+ }
+
+ #[test]
fn prover_policy_requires_configured_sp1_identity_for_local_cpu() {
let missing_identity = TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::LocalCpuProve,
proof_mode: RadrootsSp1TradeProofMode::Core,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: None,
expected_sp1_verifying_key_hash: Some(hash32('b')),
remote_http: None,
@@ -3211,6 +3286,7 @@ mod tests {
let policy = TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::LocalCpuProve,
proof_mode: RadrootsSp1TradeProofMode::Core,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: Some(hash32('a')),
expected_sp1_verifying_key_hash: Some(hash32('b')),
remote_http: None,
@@ -3239,6 +3315,7 @@ mod tests {
let missing_config = TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
proof_mode: RadrootsSp1TradeProofMode::Core,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: Some(hash32('a')),
expected_sp1_verifying_key_hash: Some(hash32('b')),
remote_http: None,
@@ -3251,6 +3328,7 @@ mod tests {
let missing_identity = TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
proof_mode: RadrootsSp1TradeProofMode::Core,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: None,
expected_sp1_verifying_key_hash: Some(hash32('b')),
remote_http: Some(remote_http_config()),
@@ -4825,6 +4903,7 @@ mod tests {
let local_execute_policy = TradeValidationReceiptProverPolicy {
backend: TradeValidationReceiptProverBackend::LocalExecute,
proof_mode: RadrootsSp1TradeProofMode::None,
+ runtime_policy: TradeValidationReceiptRuntimePolicy::Production,
expected_sp1_program_hash: None,
expected_sp1_verifying_key_hash: None,
remote_http: None,
diff --git a/tests/source_guards.rs b/tests/source_guards.rs
@@ -130,6 +130,42 @@ fn rhi_processed_job_state_is_durable_workflow_authority() {
}
}
+#[test]
+fn rhi_deterministic_prover_is_repo_local_only() {
+ let config = read_repo_file("src/config.rs");
+ let receipt_worker = read_repo_file("src/features/trade_validation_receipt.rs");
+
+ for required in [
+ "TradeValidationReceiptRuntimePolicy",
+ "RepoLocalDevelopment",
+ "repo_local_deterministic_none",
+ "DeterministicNoneRequiresRepoLocalDevelopment",
+ "RepoLocalDevelopmentPolicyRequiresDeterministicNone",
+ ] {
+ assert!(
+ receipt_worker.contains(required),
+ "trade validation receipt policy must retain deterministic backend governance `{required}`"
+ );
+ }
+
+ assert!(
+ !receipt_worker.contains("pub fn deterministic_none("),
+ "RHI must not expose an ambiguous deterministic_none constructor"
+ );
+
+ for required in [
+ "validate_trade_validation_receipt_runtime_profile",
+ "RadrootsPathProfile::RepoLocal",
+ "runtime_policy repo_local_development",
+ "runtime profile repo_local",
+ ] {
+ assert!(
+ config.contains(required),
+ "RHI config loading must retain repo-local deterministic governance `{required}`"
+ );
+ }
+}
+
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())