rhi

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

commit 7375c2234aa8334765c3249198349f75085626f8
parent 77c5b2162a0b7637a928e821ef19c5e00d872bf7
Author: triesap <tyson@radroots.org>
Date:   Wed, 20 May 2026 02:03:06 +0000

rhi: configure validation receipt prover policy

- move validation receipt backend authority into typed RHI policy
- reject request backend overrides and disabled policy before relay fetch
- report execute checks and proof verification distinctly in results
- thread configured proof policy through subscriber startup and tests

Diffstat:
MCargo.lock | 1+
Msrc/config.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/features/trade_listing/handlers/dvm.rs | 27++++++++++++++++++++++++---
Msrc/features/trade_listing/subscriber.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/features/trade_validation_receipt.rs | 585+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/lib.rs | 13++++++++++---
Msrc/main.rs | 2++
Msrc/rhi.rs | 43++++++++++++++++++++++++++++++++++++++++++-
8 files changed, 665 insertions(+), 164 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3418,6 +3418,7 @@ dependencies = [ name = "radroots_trade" version = "0.1.0-alpha.2" dependencies = [ + "base64 0.22.1", "hex", "radroots_core", "radroots_events", diff --git a/src/config.rs b/src/config.rs @@ -4,6 +4,7 @@ use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; +use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; use crate::paths::{ RhiRuntimePaths, default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver, }; @@ -47,6 +48,8 @@ pub struct Configuration { pub service: RadrootsNostrServiceConfig, #[serde(default)] pub subscriber: SubscriberConfig, + #[serde(default)] + pub trade_validation_receipt: TradeValidationReceiptProverPolicy, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -130,6 +133,8 @@ struct RawConfiguration { pub service: RawServiceConfig, #[serde(default)] pub subscriber: RawSubscriberConfig, + #[serde(default)] + pub trade_validation_receipt: TradeValidationReceiptProverPolicy, } #[derive(Debug, Deserialize, Clone)] @@ -145,6 +150,7 @@ impl RawSettings { config: Configuration { service: self.config.service.into_service_config(paths), subscriber: self.config.subscriber.into_subscriber_config(paths), + trade_validation_receipt: self.config.trade_validation_receipt, }, } } @@ -183,6 +189,7 @@ 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::paths::{ default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver, runtime_contract_with_resolver, @@ -191,6 +198,7 @@ mod tests { RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, RadrootsRuntimeNamespace, }; + use radroots_sp1_host_trade::RadrootsSp1TradeProofMode; use std::path::PathBuf; fn linux_resolver() -> RadrootsPathResolver { @@ -349,6 +357,52 @@ replay_overlap_secs = 45 ); assert_eq!(settings.config.subscriber.state.replay_window_secs, 123); assert_eq!(settings.config.subscriber.state.replay_overlap_secs, 45); + assert_eq!( + settings.config.trade_validation_receipt.backend, + TradeValidationReceiptProverBackend::Disabled + ); + assert_eq!( + settings.config.trade_validation_receipt.proof_mode, + RadrootsSp1TradeProofMode::None + ); + } + + #[test] + fn load_settings_parses_trade_validation_receipt_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" + +[config] +relays = ["wss://relay.example.com"] + +[config.trade_validation_receipt] +backend = "deterministic_none" +proof_mode = "none" +"#, + ) + .expect("write config"); + + let settings = load_settings_from_path_with_resolver( + &config_path, + &linux_resolver(), + RadrootsPathProfile::InteractiveUser, + None, + ) + .expect("load settings"); + + assert_eq!( + settings.config.trade_validation_receipt.backend, + TradeValidationReceiptProverBackend::DeterministicNone + ); + assert_eq!( + settings.config.trade_validation_receipt.proof_mode, + RadrootsSp1TradeProofMode::None + ); } #[test] diff --git a/src/features/trade_listing/handlers/dvm.rs b/src/features/trade_listing/handlers/dvm.rs @@ -52,7 +52,8 @@ use crate::features::trade_listing::state::{ TradeListingState, TradeListingStateError, TradeOrderState, }; use crate::features::trade_validation_receipt::{ - TradeValidationReceiptJobError, handle_trade_validation_receipt_job_request, + TradeValidationReceiptJobError, TradeValidationReceiptProverPolicy, + handle_trade_validation_receipt_job_request, }; #[derive(Debug, Error)] @@ -256,12 +257,13 @@ fn validate_listing_event_io( Ok(validated) } -pub async fn handle_event( +pub async fn handle_event_with_policy( event: RadrootsNostrEvent, _tags: Vec<RadrootsNostrTag>, keys: RadrootsNostrKeys, client: RadrootsNostrClient, state: Arc<tokio::sync::Mutex<TradeListingState>>, + proof_policy: &TradeValidationReceiptProverPolicy, ) -> Result<(), TradeListingDvmError> { let kind = match event.kind { RadrootsNostrKind::Custom(v) => u32::from(v), @@ -280,7 +282,7 @@ pub async fn handle_event( } if kind == KIND_WORKER_TRADE_TRANSITION_PROOF_REQ { - return handle_trade_validation_receipt_job_request(&event, &keys, &client) + return handle_trade_validation_receipt_job_request(&event, &keys, &client, proof_policy) .await .map_err(map_trade_validation_receipt_job_error); } @@ -479,6 +481,25 @@ pub async fn handle_event( Ok(()) } +#[cfg(test)] +pub async fn handle_event( + event: RadrootsNostrEvent, + tags: Vec<RadrootsNostrTag>, + keys: RadrootsNostrKeys, + client: RadrootsNostrClient, + state: Arc<tokio::sync::Mutex<TradeListingState>>, +) -> Result<(), TradeListingDvmError> { + handle_event_with_policy( + event, + tags, + keys, + client, + state, + &TradeValidationReceiptProverPolicy::default(), + ) + .await +} + fn map_trade_validation_receipt_job_error( error: TradeValidationReceiptJobError, ) -> TradeListingDvmError { diff --git a/src/features/trade_listing/subscriber.rs b/src/features/trade_listing/subscriber.rs @@ -18,9 +18,10 @@ use tokio::time::sleep; use tracing::{info, warn}; use crate::features::trade_listing::{ - handlers::dvm::{TradeListingDvmError, handle_error, handle_event}, + handlers::dvm::{TradeListingDvmError, handle_error, handle_event_with_policy}, state::{SharedTradeListingState, TradeListingRuntime}, }; +use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; #[cfg(test)] #[derive(Default)] @@ -158,10 +159,13 @@ async fn handle_event_io( keys: RadrootsNostrKeys, client: RadrootsNostrClient, state: SharedTradeListingState, + proof_policy: TradeValidationReceiptProverPolicy, ) -> Result<(), TradeListingDvmError> { let result = match take_handle_event_hook() { Some(result) => result, - None => handle_event(event, resolved_tags, keys, client, state).await, + None => { + handle_event_with_policy(event, resolved_tags, keys, client, state, &proof_policy).await + } }; result?; Ok(()) @@ -193,6 +197,7 @@ async fn process_event_notification( keys: RadrootsNostrKeys, client: RadrootsNostrClient, runtime: TradeListingRuntime, + proof_policy: TradeValidationReceiptProverPolicy, ) -> Result<()> { let created_at = u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX); if should_delay_before_event_handle() { @@ -218,6 +223,7 @@ async fn process_event_notification( keys, client.clone(), state.clone(), + proof_policy, ) .await { @@ -246,14 +252,16 @@ async fn dispatch_event_processing( keys: RadrootsNostrKeys, client: RadrootsNostrClient, runtime: TradeListingRuntime, + proof_policy: TradeValidationReceiptProverPolicy, ) -> Result<()> { - process_event_notification(event, keys, client, runtime).await + process_event_notification(event, keys, client, runtime, proof_policy).await } pub async fn subscriber( client: RadrootsNostrClient, keys: RadrootsNostrKeys, runtime: TradeListingRuntime, + proof_policy: TradeValidationReceiptProverPolicy, mut stop_rx: watch::Receiver<bool>, ) -> Result<()> { let subscribed_kinds = [KIND_LISTING, KIND_LISTING_DRAFT] @@ -306,7 +314,8 @@ pub async fn subscriber( let keys = keys.clone(); let client = client.clone(); let runtime = runtime.clone(); - dispatch_event_processing(event, keys, client, runtime).await?; + let proof_policy = proof_policy.clone(); + dispatch_event_processing(event, keys, client, runtime, proof_policy).await?; } } } @@ -328,6 +337,7 @@ mod tests { }; use crate::features::trade_listing::handlers::dvm::TradeListingDvmError; use crate::features::trade_listing::state::TradeListingRuntime; + use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; use radroots_nostr::error::RadrootsNostrTagsResolveError; use radroots_nostr::prelude::{ RadrootsNostrClient, RadrootsNostrEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, @@ -368,6 +378,10 @@ mod tests { TradeListingRuntime::new() } + fn proof_policy() -> TradeValidationReceiptProverPolicy { + TradeValidationReceiptProverPolicy::default() + } + #[test] fn notification_recv_result_mapping_covers_ok_and_err() { let keys = RadrootsNostrKeys::generate(); @@ -403,7 +417,8 @@ mod tests { Vec::new(), keys.clone(), client.clone(), - state.clone() + state.clone(), + proof_policy() ) .await, Err(TradeListingDvmError::UnsupportedKind) @@ -419,7 +434,8 @@ mod tests { Vec::new(), keys.clone(), client.clone(), - state + state, + proof_policy() ) .await .is_ok() @@ -444,7 +460,11 @@ mod tests { let keys = RadrootsNostrKeys::generate(); let client = RadrootsNostrClient::new(keys.clone()); let (_tx, rx) = watch::channel(true); - assert!(subscriber(client, keys, shared_runtime(), rx).await.is_ok()); + assert!( + subscriber(client, keys, shared_runtime(), proof_policy(), rx) + .await + .is_ok() + ); } #[tokio::test] @@ -461,15 +481,21 @@ mod tests { let (_tx_first, rx_first) = watch::channel(true); assert!( - subscriber(client.clone(), keys.clone(), runtime.clone(), rx_first) - .await - .is_ok() + subscriber( + client.clone(), + keys.clone(), + runtime.clone(), + proof_policy(), + rx_first + ) + .await + .is_ok() ); assert!(state.lock().await.is_listing_validated("addr")); let (_tx_second, rx_second) = watch::channel(true); assert!( - subscriber(client, keys, runtime.clone(), rx_second) + subscriber(client, keys, runtime.clone(), proof_policy(), rx_second) .await .is_ok() ); @@ -482,7 +508,7 @@ mod tests { let keys = RadrootsNostrKeys::generate(); let client = RadrootsNostrClient::new(keys.clone()); let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), rx) + let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) .await .expect_err("expected relay error"); let msg = format!("{err:#}"); @@ -496,7 +522,13 @@ mod tests { let client = RadrootsNostrClient::new(keys.clone()); let _ = client.add_relay("wss://relay.example.com").await; let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber(client, keys, shared_runtime(), rx)); + let join = tokio::spawn(subscriber( + client, + keys, + shared_runtime(), + proof_policy(), + rx, + )); tokio::time::sleep(std::time::Duration::from_millis(20)).await; let _ = tx.send(true); let _ = join.await; @@ -514,7 +546,7 @@ mod tests { .notifications .push_back(Err(())); let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), rx) + let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) .await .expect_err("closed notifications"); let msg = format!("{err:#}"); @@ -535,7 +567,13 @@ mod tests { .push_back(Ok(scripted_shutdown_notification())); let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber(client, keys, shared_runtime(), rx)); + let join = tokio::spawn(subscriber( + client, + keys, + shared_runtime(), + proof_policy(), + rx, + )); tokio::time::sleep(std::time::Duration::from_millis(30)).await; let _ = tx.send(true); let result = join.await.expect("subscriber join"); @@ -561,7 +599,13 @@ mod tests { "resolve-failed".to_string(), ))); let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber(client, keys, shared_runtime(), rx)); + let join = tokio::spawn(subscriber( + client, + keys, + shared_runtime(), + proof_policy(), + rx, + )); tokio::time::sleep(std::time::Duration::from_millis(30)).await; let _ = tx.send(true); let _ = join.await; @@ -597,7 +641,13 @@ mod tests { drop(hooks); let (tx, rx) = watch::channel(false); - let join = tokio::spawn(subscriber(client, keys, shared_runtime(), rx)); + let join = tokio::spawn(subscriber( + client, + keys, + shared_runtime(), + proof_policy(), + rx, + )); tokio::time::sleep(std::time::Duration::from_millis(40)).await; let _ = tx.send(true); let _ = join.await; @@ -628,7 +678,7 @@ mod tests { drop(hooks); let (_tx, rx) = watch::channel(false); - let err = subscriber(client, keys, shared_runtime(), rx) + let err = subscriber(client, keys, shared_runtime(), proof_policy(), rx) .await .expect_err("notifications closed"); let msg = format!("{err:#}"); @@ -656,7 +706,7 @@ mod tests { hooks.handle_error_results.push_back(Ok(())); drop(hooks); - process_event_notification(event, keys, client, runtime.clone()) + process_event_notification(event, keys, client, runtime.clone(), proof_policy()) .await .expect("notification"); @@ -688,7 +738,7 @@ mod tests { .push_back(Err(TradeListingDvmError::InvalidOrder)); drop(hooks); - process_event_notification(event, keys, client, runtime) + process_event_notification(event, keys, client, runtime, proof_policy()) .await .expect("processing"); } @@ -718,10 +768,16 @@ mod tests { hooks.handle_error_results.push_back(Ok(())); drop(hooks); - process_event_notification(event_ok, keys.clone(), client.clone(), runtime.clone()) - .await - .expect("ok path"); - process_event_notification(event_err, keys, client, runtime) + process_event_notification( + event_ok, + keys.clone(), + client.clone(), + runtime.clone(), + proof_policy(), + ) + .await + .expect("ok path"); + process_event_notification(event_err, keys, client, runtime, proof_policy()) .await .expect("error path"); } diff --git a/src/features/trade_validation_receipt.rs b/src/features/trade_validation_receipt.rs @@ -27,8 +27,9 @@ use radroots_sp1_guest_trade::{ RadrootsSp1TradeOrderItemWitness, RadrootsSp1TradeOrderRequestWitness, }; use radroots_sp1_host_trade::{ - RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, generate_order_acceptance_proof, - validation_receipt_for_order_acceptance_proof, verify_order_acceptance_proof_artifact, + RadrootsSp1TradeHostError, RadrootsSp1TradeProofBundle, RadrootsSp1TradeProofMode, + generate_order_acceptance_proof, validation_receipt_for_order_acceptance_proof, + verify_order_acceptance_proof_artifact, }; use radroots_trade::validation_receipt::{ RadrootsValidationReceiptError, RadrootsValidationReceiptExpectedBinding, @@ -46,7 +47,6 @@ pub struct TradeValidationReceiptJobRequest { pub listing_event_id: String, pub request_event_id: String, pub decision_event_id: String, - pub prover_backend: TradeValidationReceiptProverBackend, pub inventory_bins: Vec<RadrootsSp1TradeInventoryBinWitness>, pub inventory_sequence: u128, pub previous_state_root: Option<String>, @@ -83,12 +83,139 @@ impl TradeValidationReceiptProverBackend { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] +pub struct TradeValidationReceiptProverPolicy { + pub backend: TradeValidationReceiptProverBackend, + pub proof_mode: RadrootsSp1TradeProofMode, + #[serde(default)] + pub expected_sp1_program_hash: Option<String>, + #[serde(default)] + pub expected_sp1_verifying_key_hash: Option<String>, +} + +impl Default for TradeValidationReceiptProverPolicy { + fn default() -> Self { + Self::disabled() + } +} + +impl TradeValidationReceiptProverPolicy { + pub fn disabled() -> Self { + Self { + backend: TradeValidationReceiptProverBackend::Disabled, + proof_mode: RadrootsSp1TradeProofMode::None, + expected_sp1_program_hash: None, + expected_sp1_verifying_key_hash: None, + } + } + + pub fn deterministic_none() -> Self { + Self { + backend: TradeValidationReceiptProverBackend::DeterministicNone, + proof_mode: RadrootsSp1TradeProofMode::None, + expected_sp1_program_hash: None, + expected_sp1_verifying_key_hash: None, + } + } + + pub fn validate(&self) -> Result<(), TradeValidationReceiptJobError> { + validate_optional_hash32(&self.expected_sp1_program_hash)?; + validate_optional_hash32(&self.expected_sp1_verifying_key_hash)?; + match self.backend { + TradeValidationReceiptProverBackend::Disabled => { + if self.proof_mode != RadrootsSp1TradeProofMode::None { + return Err(TradeValidationReceiptJobError::ProverBackendDisabled); + } + if self.expected_sp1_program_hash.is_some() + || self.expected_sp1_verifying_key_hash.is_some() + { + return Err( + TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof, + ); + } + Ok(()) + } + TradeValidationReceiptProverBackend::DeterministicNone + | TradeValidationReceiptProverBackend::LocalExecute => { + if self.proof_mode != RadrootsSp1TradeProofMode::None { + return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone); + } + if self.expected_sp1_program_hash.is_some() + || self.expected_sp1_verifying_key_hash.is_some() + { + return Err( + TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof, + ); + } + if self.backend == TradeValidationReceiptProverBackend::LocalExecute + && !cfg!(feature = "sp1_proving") + { + return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( + self.backend.as_str(), + )); + } + Ok(()) + } + TradeValidationReceiptProverBackend::LocalCpuProve => { + if self.proof_mode == RadrootsSp1TradeProofMode::None { + return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof); + } + if self.proof_mode != RadrootsSp1TradeProofMode::Core { + return Err(TradeValidationReceiptJobError::UnsupportedProofMode); + } + if self.expected_sp1_program_hash.is_none() + || self.expected_sp1_verifying_key_hash.is_none() + { + return Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired); + } + if !cfg!(feature = "sp1_proving") { + return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( + self.backend.as_str(), + )); + } + Ok(()) + } + TradeValidationReceiptProverBackend::LocalCudaProve + | TradeValidationReceiptProverBackend::RemoteHttpProve => Err( + TradeValidationReceiptJobError::ProverBackendUnavailable(self.backend.as_str()), + ), + } + } + + pub fn validate_request( + &self, + request: &TradeValidationReceiptJobRequest, + ) -> Result<(), TradeValidationReceiptJobError> { + if request.proof_mode != self.proof_mode { + return Err(TradeValidationReceiptJobError::ProverBackendPolicyMismatch); + } + if self.proof_mode == RadrootsSp1TradeProofMode::None { + if request.sp1_program_hash.is_some() || request.sp1_verifying_key_hash.is_some() { + return Err(TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof); + } + return Ok(()); + } + if request.sp1_program_hash.as_deref() != self.expected_sp1_program_hash.as_deref() { + return Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch); + } + if request.sp1_verifying_key_hash.as_deref() + != self.expected_sp1_verifying_key_hash.as_deref() + { + return Err(TradeValidationReceiptJobError::ExpectedSp1VerifyingKeyHashMismatch); + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TradeValidationReceiptJobResult { + pub cryptographic_proof_verified: bool, pub decision_event_id: String, pub event_set_root: String, pub listing_event_id: String, pub order_id: String, pub proof_generated: bool, + pub proof_mode: RadrootsSp1TradeProofMode, pub proof_system: String, pub public_values_hash: String, pub prover_backend: TradeValidationReceiptProverBackend, @@ -96,6 +223,8 @@ pub struct TradeValidationReceiptJobResult { pub receipt_kind: u32, pub reducer_output_root: String, pub request_event_id: String, + pub sp1_execute_checked: bool, + pub sp1_execute_public_values_hash: Option<String>, pub status: TradeValidationReceiptJobStatus, pub worker_role: TradeValidationReceiptWorkerRole, } @@ -146,8 +275,16 @@ pub enum TradeValidationReceiptJobError { ProverBackendRequiresNone, #[error("rhi prover backend requires an SP1 proof mode")] ProverBackendRequiresSp1Proof, + #[error("rhi prover backend does not match configured policy")] + ProverBackendPolicyMismatch, #[error("rhi prover backend {0} is unavailable in this build")] ProverBackendUnavailable(&'static str), + #[error("configured SP1 identity policy is required for this prover backend")] + Sp1IdentityPolicyRequired, + #[error("expected SP1 program hash does not match configured policy")] + ExpectedSp1ProgramHashMismatch, + #[error("expected SP1 verifying key hash does not match configured policy")] + ExpectedSp1VerifyingKeyHashMismatch, #[error("nostr error: {0}")] Nostr(#[from] radroots_nostr::error::RadrootsNostrError), #[error("serde error: {0}")] @@ -162,6 +299,7 @@ pub async fn handle_trade_validation_receipt_job_request( event: &RadrootsNostrEvent, keys: &RadrootsNostrKeys, client: &RadrootsNostrClient, + prover_policy: &TradeValidationReceiptProverPolicy, ) -> Result<(), TradeValidationReceiptJobError> { let kind = event_kind_u32(event)?; if kind != KIND_WORKER_TRADE_TRANSITION_PROOF_REQ { @@ -173,8 +311,13 @@ pub async fn handle_trade_validation_receipt_job_request( return Err(TradeValidationReceiptJobError::MissingRecipient); } + prover_policy.validate()?; + if prover_policy.backend == TradeValidationReceiptProverBackend::Disabled { + return Err(TradeValidationReceiptJobError::ProverBackendDisabled); + } let request: TradeValidationReceiptJobRequest = serde_json::from_str(&event.content)?; validate_job_request_shape(&request)?; + prover_policy.validate_request(&request)?; let listing_event = fetch_event_by_id_io(client, &request.listing_event_id).await?; let order_request_event = fetch_event_by_id_io(client, &request.request_event_id).await?; @@ -242,11 +385,12 @@ pub async fn handle_trade_validation_receipt_job_request( sp1_program_hash: request.sp1_program_hash.clone(), sp1_verifying_key_hash: request.sp1_verifying_key_hash.clone(), }; - let proof_backend = request.prover_backend; - let (bundle, proof_generated) = - proof_bundle_for_backend(&witness, request.proof_mode, proof_backend).await?; - verify_order_acceptance_proof_artifact(&bundle.execution, &bundle.proof)?; - let receipt = validation_receipt_for_order_acceptance_proof(&bundle)?; + let proof_outcome = proof_bundle_for_policy(&witness, prover_policy).await?; + verify_order_acceptance_proof_artifact( + &proof_outcome.bundle.execution, + &proof_outcome.bundle.proof, + )?; + let receipt = validation_receipt_for_order_acceptance_proof(&proof_outcome.bundle)?; let receipt_parts = validation_receipt_event_build(&witness.request.order_id, &receipt)?; let verified_receipt = verify_validation_receipt_event( &radroots_events::RadrootsNostrEvent { @@ -261,11 +405,11 @@ pub async fn handle_trade_validation_receipt_job_request( RadrootsValidationReceiptExpectedBinding { event_set_root: Some(&receipt.event_set_root), order_id: Some(&witness.request.order_id), - program_hash: receipt.proof.program_hash.as_deref(), + program_hash: prover_policy.expected_sp1_program_hash.as_deref(), proof_system: Some(receipt.proof.system), public_values_hash: Some(&receipt.public_values_hash), reducer_output_root: Some(&receipt.new_state_root), - verifying_key_hash: receipt.proof.verifying_key_hash.as_deref(), + verifying_key_hash: prover_policy.expected_sp1_verifying_key_hash.as_deref(), }, )?; let receipt_event_id = publish_event_parts_io( @@ -277,18 +421,22 @@ pub async fn handle_trade_validation_receipt_job_request( .await?; let result = TradeValidationReceiptJobResult { + cryptographic_proof_verified: proof_outcome.cryptographic_proof_verified, decision_event_id: request.decision_event_id, event_set_root: verified_receipt.receipt.event_set_root, listing_event_id: request.listing_event_id, order_id: witness.request.order_id, - proof_generated, + proof_generated: proof_outcome.proof_generated, + proof_mode: prover_policy.proof_mode, proof_system: verified_receipt.receipt.proof.system.as_str().to_string(), public_values_hash: verified_receipt.receipt.public_values_hash, - prover_backend: proof_backend, + prover_backend: prover_policy.backend, receipt_event_id: receipt_event_id.clone(), receipt_kind: KIND_TRADE_VALIDATION_RECEIPT, reducer_output_root: verified_receipt.receipt.new_state_root, request_event_id: request.request_event_id, + sp1_execute_checked: proof_outcome.sp1_execute_checked, + sp1_execute_public_values_hash: proof_outcome.sp1_execute_public_values_hash, status: TradeValidationReceiptJobStatus::Succeeded, worker_role: TradeValidationReceiptWorkerRole::NonAuthoritativeProver, }; @@ -439,41 +587,43 @@ fn order_decision_witness_from_payload( } } -async fn proof_bundle_for_backend( +struct TradeValidationReceiptProofOutcome { + bundle: RadrootsSp1TradeProofBundle, + proof_generated: bool, + sp1_execute_checked: bool, + sp1_execute_public_values_hash: Option<String>, + cryptographic_proof_verified: bool, +} + +async fn proof_bundle_for_policy( witness: &RadrootsSp1TradeOrderAcceptanceWitness, - proof_mode: RadrootsSp1TradeProofMode, - backend: TradeValidationReceiptProverBackend, -) -> Result< - (radroots_sp1_host_trade::RadrootsSp1TradeProofBundle, bool), - TradeValidationReceiptJobError, -> { - match backend { + policy: &TradeValidationReceiptProverPolicy, +) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> { + match policy.backend { TradeValidationReceiptProverBackend::Disabled => { Err(TradeValidationReceiptJobError::ProverBackendDisabled) } TradeValidationReceiptProverBackend::DeterministicNone => { - if proof_mode != RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone); - } - Ok((generate_order_acceptance_proof(witness, proof_mode)?, false)) + let bundle = generate_order_acceptance_proof(witness, policy.proof_mode)?; + Ok(TradeValidationReceiptProofOutcome { + bundle, + proof_generated: false, + sp1_execute_checked: false, + sp1_execute_public_values_hash: None, + cryptographic_proof_verified: false, + }) } TradeValidationReceiptProverBackend::LocalExecute => { - if proof_mode != RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone); - } - run_local_execute_backend(witness, proof_mode).await + run_local_execute_backend(witness, policy.proof_mode).await } TradeValidationReceiptProverBackend::LocalCpuProve => { - if proof_mode == RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof); - } - run_local_cpu_prove_backend(witness, proof_mode).await + run_local_cpu_prove_backend(witness, policy.proof_mode).await } TradeValidationReceiptProverBackend::LocalCudaProve => Err( - TradeValidationReceiptJobError::ProverBackendUnavailable(backend.as_str()), + TradeValidationReceiptJobError::ProverBackendUnavailable(policy.backend.as_str()), ), TradeValidationReceiptProverBackend::RemoteHttpProve => Err( - TradeValidationReceiptJobError::ProverBackendUnavailable(backend.as_str()), + TradeValidationReceiptJobError::ProverBackendUnavailable(policy.backend.as_str()), ), } } @@ -482,22 +632,24 @@ async fn proof_bundle_for_backend( async fn run_local_execute_backend( witness: &RadrootsSp1TradeOrderAcceptanceWitness, proof_mode: RadrootsSp1TradeProofMode, -) -> Result< - (radroots_sp1_host_trade::RadrootsSp1TradeProofBundle, bool), - TradeValidationReceiptJobError, -> { - let _ = radroots_sp1_host_trade::execute_order_acceptance_sp1_public_values(witness).await?; - Ok((generate_order_acceptance_proof(witness, proof_mode)?, false)) +) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> { + let sp1_execution = + radroots_sp1_host_trade::execute_order_acceptance_sp1_public_values(witness).await?; + let bundle = generate_order_acceptance_proof(witness, proof_mode)?; + Ok(TradeValidationReceiptProofOutcome { + bundle, + proof_generated: false, + sp1_execute_checked: true, + sp1_execute_public_values_hash: Some(sp1_execution.execution.public_values_hash), + cryptographic_proof_verified: false, + }) } #[cfg(not(feature = "sp1_proving"))] async fn run_local_execute_backend( _witness: &RadrootsSp1TradeOrderAcceptanceWitness, _proof_mode: RadrootsSp1TradeProofMode, -) -> Result< - (radroots_sp1_host_trade::RadrootsSp1TradeProofBundle, bool), - TradeValidationReceiptJobError, -> { +) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> { Err(TradeValidationReceiptJobError::ProverBackendUnavailable( TradeValidationReceiptProverBackend::LocalExecute.as_str(), )) @@ -507,10 +659,7 @@ async fn run_local_execute_backend( async fn run_local_cpu_prove_backend( witness: &RadrootsSp1TradeOrderAcceptanceWitness, proof_mode: RadrootsSp1TradeProofMode, -) -> Result< - (radroots_sp1_host_trade::RadrootsSp1TradeProofBundle, bool), - TradeValidationReceiptJobError, -> { +) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> { let bundle = radroots_sp1_host_trade::generate_order_acceptance_sp1_proof(witness, proof_mode).await?; radroots_sp1_host_trade::verify_order_acceptance_sp1_proof_artifact( @@ -518,17 +667,20 @@ async fn run_local_cpu_prove_backend( &bundle.proof, ) .await?; - Ok((bundle, true)) + Ok(TradeValidationReceiptProofOutcome { + sp1_execute_public_values_hash: Some(bundle.execution.public_values_hash.clone()), + bundle, + proof_generated: true, + sp1_execute_checked: true, + cryptographic_proof_verified: true, + }) } #[cfg(not(feature = "sp1_proving"))] async fn run_local_cpu_prove_backend( _witness: &RadrootsSp1TradeOrderAcceptanceWitness, _proof_mode: RadrootsSp1TradeProofMode, -) -> Result< - (radroots_sp1_host_trade::RadrootsSp1TradeProofBundle, bool), - TradeValidationReceiptJobError, -> { +) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> { Err(TradeValidationReceiptJobError::ProverBackendUnavailable( TradeValidationReceiptProverBackend::LocalCpuProve.as_str(), )) @@ -561,53 +713,6 @@ fn validate_job_request_shape( } validate_optional_hash32(&request.sp1_program_hash)?; validate_optional_hash32(&request.sp1_verifying_key_hash)?; - validate_proof_config(request)?; - Ok(()) -} - -fn validate_proof_config( - request: &TradeValidationReceiptJobRequest, -) -> Result<(), TradeValidationReceiptJobError> { - match request.prover_backend { - TradeValidationReceiptProverBackend::Disabled => { - return Err(TradeValidationReceiptJobError::ProverBackendDisabled); - } - TradeValidationReceiptProverBackend::DeterministicNone - | TradeValidationReceiptProverBackend::LocalExecute => { - if request.proof_mode != RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone); - } - if request.sp1_program_hash.is_some() || request.sp1_verifying_key_hash.is_some() { - return Err(TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof); - } - if request.prover_backend == TradeValidationReceiptProverBackend::LocalExecute - && !cfg!(feature = "sp1_proving") - { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - request.prover_backend.as_str(), - )); - } - } - TradeValidationReceiptProverBackend::LocalCpuProve => { - if request.proof_mode == RadrootsSp1TradeProofMode::None { - return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof); - } - if request.proof_mode != RadrootsSp1TradeProofMode::Core { - return Err(TradeValidationReceiptJobError::UnsupportedProofMode); - } - if !cfg!(feature = "sp1_proving") { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - request.prover_backend.as_str(), - )); - } - } - TradeValidationReceiptProverBackend::LocalCudaProve - | TradeValidationReceiptProverBackend::RemoteHttpProve => { - return Err(TradeValidationReceiptJobError::ProverBackendUnavailable( - request.prover_backend.as_str(), - )); - } - } Ok(()) } @@ -672,6 +777,22 @@ fn result_tags( "prover_backend".to_string(), result.prover_backend.as_str().to_string(), ], + vec![ + "proof_mode".to_string(), + result.proof_mode.mode_label().unwrap_or("none").to_string(), + ], + vec![ + "proof_generated".to_string(), + result.proof_generated.to_string(), + ], + vec![ + "sp1_execute_checked".to_string(), + result.sp1_execute_checked.to_string(), + ], + vec![ + "cryptographic_proof_verified".to_string(), + result.cryptographic_proof_verified.to_string(), + ], ] } @@ -781,8 +902,8 @@ mod tests { use super::{ TradeValidationReceiptJobError, TradeValidationReceiptJobRequest, TradeValidationReceiptJobResult, TradeValidationReceiptProverBackend, - TradeValidationReceiptTestHooks, handle_trade_validation_receipt_job_request, - trade_validation_receipt_test_hooks, + TradeValidationReceiptProverPolicy, TradeValidationReceiptTestHooks, + handle_trade_validation_receipt_job_request, trade_validation_receipt_test_hooks, }; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, @@ -970,7 +1091,6 @@ mod tests { listing_event: &RadrootsNostrEvent, request_event: &RadrootsNostrEvent, decision_event: &RadrootsNostrEvent, - prover_backend: TradeValidationReceiptProverBackend, proof_mode: RadrootsSp1TradeProofMode, sp1_verifying_key_hash: Option<String>, ) -> RadrootsNostrEvent { @@ -982,7 +1102,6 @@ mod tests { listing_event_id: listing_event.id.to_hex(), request_event_id: request_event.id.to_hex(), decision_event_id: decision_event.id.to_hex(), - prover_backend, inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { bin_id: "bin-1".to_string(), listing_capacity: 5, @@ -1008,6 +1127,68 @@ mod tests { RadrootsNostrClient::new(keys.clone()) } + fn deterministic_policy() -> TradeValidationReceiptProverPolicy { + TradeValidationReceiptProverPolicy::deterministic_none() + } + + fn hash32(ch: char) -> String { + format!("0x{}", ch.to_string().repeat(64)) + } + + #[test] + fn prover_policy_requires_configured_sp1_identity_for_local_cpu() { + let missing_identity = TradeValidationReceiptProverPolicy { + backend: TradeValidationReceiptProverBackend::LocalCpuProve, + proof_mode: RadrootsSp1TradeProofMode::Core, + expected_sp1_program_hash: None, + expected_sp1_verifying_key_hash: Some(hash32('b')), + }; + assert!(matches!( + missing_identity.validate(), + Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired) + )); + + let policy = TradeValidationReceiptProverPolicy { + backend: TradeValidationReceiptProverBackend::LocalCpuProve, + proof_mode: RadrootsSp1TradeProofMode::Core, + expected_sp1_program_hash: Some(hash32('a')), + expected_sp1_verifying_key_hash: Some(hash32('b')), + }; + let request = TradeValidationReceiptJobRequest { + witness_version: radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_WITNESS_VERSION, + proof_target: + radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET + .to_string(), + listing_event_id: "listing-event".to_string(), + request_event_id: "request-event".to_string(), + decision_event_id: "decision-event".to_string(), + inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { + bin_id: "bin-1".to_string(), + listing_capacity: 5, + previous_reserved: 1, + }], + inventory_sequence: 7, + previous_state_root: None, + proof_mode: RadrootsSp1TradeProofMode::Core, + reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), + radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), + sp1_program_hash: Some(hash32('c')), + sp1_verifying_key_hash: Some(hash32('b')), + }; + assert!(matches!( + policy.validate_request(&request), + Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch) + )); + + let mut request = request; + request.sp1_program_hash = None; + request.sp1_verifying_key_hash = None; + assert!(matches!( + policy.validate_request(&request), + Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch) + )); + } + #[tokio::test] async fn proof_job_publishes_verified_receipt_and_result_after_proof_verification() { let _guard = test_guard(); @@ -1023,7 +1204,6 @@ mod tests { &listing_event, &request_event, &decision_event, - TradeValidationReceiptProverBackend::DeterministicNone, RadrootsSp1TradeProofMode::None, None, ); @@ -1049,9 +1229,14 @@ mod tests { .push_back(Ok(publish_result_id(2))); } - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect("proof job"); + handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect("proof job"); let published = trade_validation_receipt_test_hooks() .lock() @@ -1088,6 +1273,10 @@ mod tests { TradeValidationReceiptProverBackend::DeterministicNone ); assert!(!result.proof_generated); + assert_eq!(result.proof_mode, RadrootsSp1TradeProofMode::None); + assert!(!result.sp1_execute_checked); + assert!(result.sp1_execute_public_values_hash.is_none()); + assert!(!result.cryptographic_proof_verified); assert_eq!( result.public_values_hash, verified.receipt.public_values_hash @@ -1102,6 +1291,10 @@ mod tests { tag.get(0).map(String::as_str) == Some("prover_backend") && tag.get(1).map(String::as_str) == Some("deterministic_none") })); + assert!(published[1].tags.iter().any(|tag| { + tag.get(0).map(String::as_str) == Some("proof_mode") + && tag.get(1).map(String::as_str) == Some("none") + })); } #[tokio::test] @@ -1119,7 +1312,6 @@ mod tests { &listing_event, &request_event, &decision_event, - TradeValidationReceiptProverBackend::DeterministicNone, RadrootsSp1TradeProofMode::Compressed, None, ); @@ -1135,13 +1327,17 @@ mod tests { .push_back(Ok(decision_event)); } - let error = - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect_err("backend rejects sp1 proof claim"); + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect_err("backend rejects sp1 proof claim"); assert!(matches!( error, - TradeValidationReceiptJobError::ProverBackendRequiresNone + TradeValidationReceiptJobError::ProverBackendPolicyMismatch )); assert_eq!( trade_validation_receipt_test_hooks() @@ -1161,6 +1357,110 @@ mod tests { } #[tokio::test] + async fn proof_job_rejects_request_prover_backend_override_before_relay_fetch() { + let _guard = test_guard(); + let worker = RadrootsNostrKeys::generate(); + let requester = RadrootsNostrKeys::generate(); + let buyer = RadrootsNostrKeys::generate(); + let seller = RadrootsNostrKeys::generate(); + let listing_event = listing_event(&seller); + let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event); + let job = job_request( + &requester, + &worker, + &listing_event, + &request_event, + &decision_event, + RadrootsSp1TradeProofMode::None, + None, + ); + let mut request_json: serde_json::Value = + serde_json::from_str(&job.content).expect("request json"); + request_json["prover_backend"] = serde_json::Value::String("local_cpu_prove".to_string()); + let job = signed_event( + &requester, + KIND_WORKER_TRADE_TRANSITION_PROOF_REQ, + serde_json::to_string(&request_json).expect("request json"), + vec![vec!["p".to_string(), worker.public_key().to_string()]], + ); + + { + let mut hooks = trade_validation_receipt_test_hooks() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + hooks.fetch_event_by_id_results.push_back(Ok(listing_event)); + hooks.fetch_event_by_id_results.push_back(Ok(request_event)); + hooks + .fetch_event_by_id_results + .push_back(Ok(decision_event)); + } + + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect_err("request backend override rejected"); + assert!(matches!(error, TradeValidationReceiptJobError::Serde(_))); + let hooks = trade_validation_receipt_test_hooks() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_eq!(hooks.fetch_event_by_id_results.len(), 3); + assert!(hooks.published_events.is_empty()); + } + + #[tokio::test] + async fn proof_job_rejects_disabled_policy_before_relay_fetch() { + let _guard = test_guard(); + let worker = RadrootsNostrKeys::generate(); + let requester = RadrootsNostrKeys::generate(); + let buyer = RadrootsNostrKeys::generate(); + let seller = RadrootsNostrKeys::generate(); + let listing_event = listing_event(&seller); + let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event); + let job = job_request( + &requester, + &worker, + &listing_event, + &request_event, + &decision_event, + RadrootsSp1TradeProofMode::None, + None, + ); + + { + let mut hooks = trade_validation_receipt_test_hooks() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + hooks.fetch_event_by_id_results.push_back(Ok(listing_event)); + hooks.fetch_event_by_id_results.push_back(Ok(request_event)); + hooks + .fetch_event_by_id_results + .push_back(Ok(decision_event)); + } + + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &TradeValidationReceiptProverPolicy::default(), + ) + .await + .expect_err("disabled policy rejected"); + assert!(matches!( + error, + TradeValidationReceiptJobError::ProverBackendDisabled + )); + let hooks = trade_validation_receipt_test_hooks() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_eq!(hooks.fetch_event_by_id_results.len(), 3); + assert!(hooks.published_events.is_empty()); + } + + #[tokio::test] async fn proof_job_rejects_unverified_signed_event_evidence_before_publication() { let _guard = test_guard(); let worker = RadrootsNostrKeys::generate(); @@ -1176,7 +1476,6 @@ mod tests { &listing_event, &request_event, &decision_event, - TradeValidationReceiptProverBackend::DeterministicNone, RadrootsSp1TradeProofMode::None, None, ); @@ -1193,10 +1492,14 @@ mod tests { .push_back(Ok(decision_event)); } - let error = - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect_err("signed evidence rejected"); + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect_err("signed evidence rejected"); assert!(matches!( error, TradeValidationReceiptJobError::InvalidSignedEvent @@ -1225,7 +1528,6 @@ mod tests { &listing_event, &request_event, &decision_event, - TradeValidationReceiptProverBackend::DeterministicNone, RadrootsSp1TradeProofMode::None, None, ); @@ -1251,10 +1553,14 @@ mod tests { .push_back(Ok(decision_event)); } - let error = - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect_err("identity mismatch rejected"); + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect_err("identity mismatch rejected"); assert!(matches!( error, TradeValidationReceiptJobError::ExpectedReducerProgramHashMismatch @@ -1282,7 +1588,6 @@ mod tests { &listing_event, &request_event, &decision_event, - TradeValidationReceiptProverBackend::LocalExecute, RadrootsSp1TradeProofMode::None, None, ); @@ -1298,10 +1603,20 @@ mod tests { .push_back(Ok(decision_event)); } - let error = - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect_err("backend unavailable"); + let local_execute_policy = TradeValidationReceiptProverPolicy { + backend: TradeValidationReceiptProverBackend::LocalExecute, + proof_mode: RadrootsSp1TradeProofMode::None, + expected_sp1_program_hash: None, + expected_sp1_verifying_key_hash: None, + }; + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &local_execute_policy, + ) + .await + .expect_err("backend unavailable"); assert!(matches!( error, TradeValidationReceiptJobError::ProverBackendUnavailable("local_execute") @@ -1339,10 +1654,14 @@ mod tests { .sign_with_keys(&requester) .expect("job"); - let error = - handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker)) - .await - .expect_err("missing recipient"); + let error = handle_trade_validation_receipt_job_request( + &job, + &worker, + &client_for(&worker), + &deterministic_policy(), + ) + .await + .expect_err("missing recipient"); assert!(matches!( error, TradeValidationReceiptJobError::MissingRecipient diff --git a/src/lib.rs b/src/lib.rs @@ -17,7 +17,7 @@ use std::time::Duration; use crate::features::trade_listing::state::{TradeListingRuntime, TradeListingRuntimeConfig}; use crate::identity_storage::load_service_identity; -use crate::rhi::{Rhi, start_subscriber}; +use crate::rhi::{Rhi, start_subscriber_with_policy}; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrApplicationHandlerSpec, radroots_nostr_bootstrap_service_presence, @@ -122,7 +122,11 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> }) .await?; - let rhi = Rhi::with_trade_listing_runtime(keys.clone(), trade_listing_runtime); + let rhi = Rhi::with_trade_listing_runtime_and_policy( + keys.clone(), + trade_listing_runtime, + settings.config.trade_validation_receipt.clone(), + ); let client = rhi.client.clone(); let service_cfg = settings.config.service.clone(); let relays = service_cfg.relays.clone(); @@ -158,10 +162,11 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> return Ok(()); } - let handle = start_subscriber( + let handle = start_subscriber_with_policy( client.clone(), keys.clone(), rhi.trade_listing_runtime.clone(), + rhi.trade_validation_receipt_policy.clone(), settings.config.subscriber.backoff.clone(), ) .await; @@ -241,6 +246,8 @@ mod tests { ..Default::default() }, }, + trade_validation_receipt: + crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy::default(), }, } } diff --git a/src/main.rs b/src/main.rs @@ -264,6 +264,8 @@ mod tests { nip89_extra_tags: Vec::new(), }, subscriber: config::SubscriberConfig::default(), + trade_validation_receipt: + rhi::features::trade_validation_receipt::TradeValidationReceiptProverPolicy::default(), }, } } diff --git a/src/rhi.rs b/src/rhi.rs @@ -6,6 +6,7 @@ use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys}; use radroots_runtime::{Backoff, BackoffConfig}; use crate::features::trade_listing::state::TradeListingRuntime; +use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy; #[cfg(not(test))] fn connection_wait_timeout() -> Duration { @@ -32,6 +33,7 @@ async fn run_subscriber_once( client: RadrootsNostrClient, keys: RadrootsNostrKeys, runtime: TradeListingRuntime, + proof_policy: TradeValidationReceiptProverPolicy, stop_rx: tokio::sync::watch::Receiver<bool>, ) -> Result<(), anyhow::Error> { #[cfg(test)] @@ -43,7 +45,14 @@ async fn run_subscriber_once( return result; } - crate::features::trade_listing::subscriber::subscriber(client, keys, runtime, stop_rx).await + crate::features::trade_listing::subscriber::subscriber( + client, + keys, + runtime, + proof_policy, + stop_rx, + ) + .await } async fn wait_for_connection_or_stop( @@ -63,6 +72,7 @@ pub struct Rhi { pub(crate) _started: Instant, pub client: RadrootsNostrClient, pub(crate) trade_listing_runtime: TradeListingRuntime, + pub(crate) trade_validation_receipt_policy: TradeValidationReceiptProverPolicy, } impl Rhi { @@ -74,11 +84,24 @@ impl Rhi { keys: RadrootsNostrKeys, trade_listing_runtime: TradeListingRuntime, ) -> Self { + Self::with_trade_listing_runtime_and_policy( + keys, + trade_listing_runtime, + TradeValidationReceiptProverPolicy::default(), + ) + } + + pub fn with_trade_listing_runtime_and_policy( + keys: RadrootsNostrKeys, + trade_listing_runtime: TradeListingRuntime, + trade_validation_receipt_policy: TradeValidationReceiptProverPolicy, + ) -> Self { let client = RadrootsNostrClient::new(keys); Self { _started: Instant::now(), client, trade_listing_runtime, + trade_validation_receipt_policy, } } } @@ -120,6 +143,23 @@ pub async fn start_subscriber( runtime: TradeListingRuntime, backoff_cfg: BackoffConfig, ) -> RhiHandle { + start_subscriber_with_policy( + client, + keys, + runtime, + TradeValidationReceiptProverPolicy::default(), + backoff_cfg, + ) + .await +} + +pub async fn start_subscriber_with_policy( + client: RadrootsNostrClient, + keys: RadrootsNostrKeys, + runtime: TradeListingRuntime, + proof_policy: TradeValidationReceiptProverPolicy, + backoff_cfg: BackoffConfig, +) -> RhiHandle { let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false); let join = tokio::spawn(async move { @@ -138,6 +178,7 @@ pub async fn start_subscriber( client.clone(), keys.clone(), runtime.clone(), + proof_policy.clone(), stop_rx.clone(), ) .await;