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:
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;