rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

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:
Msrc/config.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/features/trade_validation_receipt.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mtests/source_guards.rs | 36++++++++++++++++++++++++++++++++++++
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())