rhi

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

trade_validation_receipt.rs (118867B)


      1 #![forbid(unsafe_code)]
      2 #![cfg_attr(coverage_nightly, coverage(off))]
      3 
      4 use radroots_events::kinds::{
      5     KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT,
      6     KIND_TRADE_VALIDATION_RECEIPT, is_listing_kind,
      7 };
      8 use radroots_events::order::{
      9     RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderRequest,
     10 };
     11 use radroots_events_codec::order::{
     12     order_decision_from_event, order_request_from_event, parse_order_listing_event_tag,
     13     parse_order_prev_tag, parse_order_root_tag,
     14 };
     15 use radroots_nostr::prelude::{
     16     RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys,
     17     RadrootsNostrKind, radroots_event_from_nostr, radroots_nostr_build_event,
     18     radroots_nostr_fetch_event_by_id, radroots_nostr_send_event,
     19 };
     20 use radroots_sp1_guest_trade::{
     21     RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, RADROOTS_SP1_TRADE_PROTOCOL_VERSION,
     22     RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, RADROOTS_SP1_TRADE_WITNESS_VERSION,
     23     RadrootsSp1TradeCanonicalEventEvidence, RadrootsSp1TradeEventEvidenceRole,
     24     RadrootsSp1TradeEventWorkflowPosition, RadrootsSp1TradeInventoryBinWitness,
     25     RadrootsSp1TradeInventoryCommitmentWitness, RadrootsSp1TradeOrderAcceptanceWitness,
     26     RadrootsSp1TradeOrderDecisionEventWitness, RadrootsSp1TradeOrderDecisionWitness,
     27     RadrootsSp1TradeOrderItemWitness, RadrootsSp1TradeOrderRequestWitness,
     28 };
     29 use radroots_sp1_host_trade::{
     30     RadrootsSp1TradeHostError, RadrootsSp1TradeProofBundle, RadrootsSp1TradeProofMode,
     31     generate_order_acceptance_proof, validation_receipt_for_order_acceptance_proof,
     32     verify_order_acceptance_proof_artifact_structure,
     33 };
     34 use radroots_trade::validation_receipt::{
     35     RadrootsValidationReceiptError, RadrootsValidationReceiptExpectedBinding,
     36     validation_receipt_event_build, verify_validation_receipt_event,
     37 };
     38 use serde::{Deserialize, Serialize};
     39 use sha2::{Digest, Sha256};
     40 #[cfg(feature = "sp1_verify")]
     41 use std::time::Duration;
     42 use thiserror::Error;
     43 
     44 #[cfg(feature = "sp1_verify")]
     45 use radroots_sp1_host_trade::{
     46     RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, RADROOTS_SP1_TRADE_SP1_VERSION_LINE,
     47     RadrootsSp1TradeRemoteProverRequest, RadrootsSp1TradeRemoteProverResponse,
     48     RadrootsSp1TradeRemoteProverStatus, RadrootsSp1TradeResolvedProofArtifact,
     49 };
     50 
     51 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     52 #[serde(deny_unknown_fields)]
     53 pub struct TradeValidationReceiptJobRequest {
     54     pub witness_version: u32,
     55     pub proof_target: String,
     56     pub listing_event_id: String,
     57     pub request_event_id: String,
     58     pub decision_event_id: String,
     59     pub inventory_bins: Vec<RadrootsSp1TradeInventoryBinWitness>,
     60     pub inventory_sequence: u128,
     61     pub previous_state_root: Option<String>,
     62     pub proof_mode: RadrootsSp1TradeProofMode,
     63     pub reducer_program_hash: String,
     64     pub radroots_protocol_version: String,
     65     pub sp1_program_hash: Option<String>,
     66     pub sp1_verifying_key_hash: Option<String>,
     67 }
     68 
     69 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     70 #[serde(rename_all = "snake_case")]
     71 pub enum TradeValidationReceiptProverBackend {
     72     Disabled,
     73     DeterministicNone,
     74     LocalExecute,
     75     LocalCpuProve,
     76     LocalCudaProve,
     77     RemoteHttpProve,
     78 }
     79 
     80 impl TradeValidationReceiptProverBackend {
     81     pub const fn as_str(self) -> &'static str {
     82         match self {
     83             Self::Disabled => "disabled",
     84             Self::DeterministicNone => "deterministic_none",
     85             Self::LocalExecute => "local_execute",
     86             Self::LocalCpuProve => "local_cpu_prove",
     87             Self::LocalCudaProve => "local_cuda_prove",
     88             Self::RemoteHttpProve => "remote_http_prove",
     89         }
     90     }
     91 }
     92 
     93 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     94 #[serde(deny_unknown_fields)]
     95 pub struct TradeValidationReceiptProverPolicy {
     96     pub backend: TradeValidationReceiptProverBackend,
     97     pub proof_mode: RadrootsSp1TradeProofMode,
     98     #[serde(default)]
     99     pub expected_sp1_program_hash: Option<String>,
    100     #[serde(default)]
    101     pub expected_sp1_verifying_key_hash: Option<String>,
    102     #[serde(default)]
    103     pub remote_http: Option<TradeValidationReceiptRemoteHttpProverConfig>,
    104 }
    105 
    106 impl Default for TradeValidationReceiptProverPolicy {
    107     fn default() -> Self {
    108         Self::disabled()
    109     }
    110 }
    111 
    112 impl TradeValidationReceiptProverPolicy {
    113     pub fn disabled() -> Self {
    114         Self {
    115             backend: TradeValidationReceiptProverBackend::Disabled,
    116             proof_mode: RadrootsSp1TradeProofMode::None,
    117             expected_sp1_program_hash: None,
    118             expected_sp1_verifying_key_hash: None,
    119             remote_http: None,
    120         }
    121     }
    122 
    123     pub fn deterministic_none() -> Self {
    124         Self {
    125             backend: TradeValidationReceiptProverBackend::DeterministicNone,
    126             proof_mode: RadrootsSp1TradeProofMode::None,
    127             expected_sp1_program_hash: None,
    128             expected_sp1_verifying_key_hash: None,
    129             remote_http: None,
    130         }
    131     }
    132 
    133     pub fn validate(&self) -> Result<(), TradeValidationReceiptJobError> {
    134         validate_optional_hash32(&self.expected_sp1_program_hash)?;
    135         validate_optional_hash32(&self.expected_sp1_verifying_key_hash)?;
    136         match self.backend {
    137             TradeValidationReceiptProverBackend::Disabled => {
    138                 if self.proof_mode != RadrootsSp1TradeProofMode::None {
    139                     return Err(TradeValidationReceiptJobError::ProverBackendDisabled);
    140                 }
    141                 if self.expected_sp1_program_hash.is_some()
    142                     || self.expected_sp1_verifying_key_hash.is_some()
    143                 {
    144                     return Err(
    145                         TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof,
    146                     );
    147                 }
    148                 Ok(())
    149             }
    150             TradeValidationReceiptProverBackend::DeterministicNone
    151             | TradeValidationReceiptProverBackend::LocalExecute => {
    152                 if self.proof_mode != RadrootsSp1TradeProofMode::None {
    153                     return Err(TradeValidationReceiptJobError::ProverBackendRequiresNone);
    154                 }
    155                 if self.expected_sp1_program_hash.is_some()
    156                     || self.expected_sp1_verifying_key_hash.is_some()
    157                 {
    158                     return Err(
    159                         TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof,
    160                     );
    161                 }
    162                 if self.backend == TradeValidationReceiptProverBackend::LocalExecute
    163                     && !cfg!(feature = "sp1_proving")
    164                 {
    165                     return Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
    166                         self.backend.as_str(),
    167                     ));
    168                 }
    169                 Ok(())
    170             }
    171             TradeValidationReceiptProverBackend::LocalCpuProve => {
    172                 if self.proof_mode == RadrootsSp1TradeProofMode::None {
    173                     return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof);
    174                 }
    175                 if self.proof_mode != RadrootsSp1TradeProofMode::Core {
    176                     return Err(TradeValidationReceiptJobError::UnsupportedProofMode);
    177                 }
    178                 if self.expected_sp1_program_hash.is_none()
    179                     || self.expected_sp1_verifying_key_hash.is_none()
    180                 {
    181                     return Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired);
    182                 }
    183                 if !cfg!(feature = "sp1_proving") {
    184                     return Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
    185                         self.backend.as_str(),
    186                     ));
    187                 }
    188                 Ok(())
    189             }
    190             TradeValidationReceiptProverBackend::LocalCudaProve => Err(
    191                 TradeValidationReceiptJobError::ProverBackendUnavailable(self.backend.as_str()),
    192             ),
    193             TradeValidationReceiptProverBackend::RemoteHttpProve => {
    194                 if self.proof_mode == RadrootsSp1TradeProofMode::None {
    195                     return Err(TradeValidationReceiptJobError::ProverBackendRequiresSp1Proof);
    196                 }
    197                 if self.proof_mode != RadrootsSp1TradeProofMode::Core {
    198                     return Err(TradeValidationReceiptJobError::UnsupportedProofMode);
    199                 }
    200                 if self.expected_sp1_program_hash.is_none()
    201                     || self.expected_sp1_verifying_key_hash.is_none()
    202                 {
    203                     return Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired);
    204                 }
    205                 let remote_http = self
    206                     .remote_http
    207                     .as_ref()
    208                     .ok_or(TradeValidationReceiptJobError::RemoteHttpConfigRequired)?;
    209                 remote_http.validate()?;
    210                 remote_http_auth_token(remote_http)?;
    211                 if !cfg!(feature = "sp1_verify") {
    212                     return Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
    213                         self.backend.as_str(),
    214                     ));
    215                 }
    216                 Ok(())
    217             }
    218         }
    219     }
    220 
    221     pub fn validate_request(
    222         &self,
    223         request: &TradeValidationReceiptJobRequest,
    224     ) -> Result<(), TradeValidationReceiptJobError> {
    225         if request.proof_mode != self.proof_mode {
    226             return Err(TradeValidationReceiptJobError::ProverBackendPolicyMismatch);
    227         }
    228         if self.proof_mode == RadrootsSp1TradeProofMode::None {
    229             if request.sp1_program_hash.is_some() || request.sp1_verifying_key_hash.is_some() {
    230                 return Err(TradeValidationReceiptJobError::Sp1IdentityConstraintsRequireSp1Proof);
    231             }
    232             return Ok(());
    233         }
    234         if request.sp1_program_hash.as_deref() != self.expected_sp1_program_hash.as_deref() {
    235             return Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch);
    236         }
    237         if request.sp1_verifying_key_hash.as_deref()
    238             != self.expected_sp1_verifying_key_hash.as_deref()
    239         {
    240             return Err(TradeValidationReceiptJobError::ExpectedSp1VerifyingKeyHashMismatch);
    241         }
    242         Ok(())
    243     }
    244 }
    245 
    246 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    247 #[serde(deny_unknown_fields)]
    248 pub struct TradeValidationReceiptRemoteHttpProverConfig {
    249     pub endpoint_url: String,
    250     pub auth: TradeValidationReceiptRemoteHttpAuth,
    251     pub request_timeout_ms: u64,
    252     pub poll_interval_ms: u64,
    253     pub max_poll_attempts: u32,
    254     pub max_response_bytes: usize,
    255 }
    256 
    257 impl TradeValidationReceiptRemoteHttpProverConfig {
    258     pub fn validate(&self) -> Result<(), TradeValidationReceiptJobError> {
    259         let url = remote_http_endpoint_url(self)?;
    260         if url.scheme() != "http" && url.scheme() != "https" {
    261             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    262                 "endpoint_url",
    263             ));
    264         }
    265         if matches!(
    266             self.auth,
    267             TradeValidationReceiptRemoteHttpAuth::BearerTokenEnv { .. }
    268         ) && url.scheme() != "https"
    269         {
    270             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    271                 "auth.endpoint_url_scheme",
    272             ));
    273         }
    274         if self.request_timeout_ms == 0 {
    275             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    276                 "request_timeout_ms",
    277             ));
    278         }
    279         if self.poll_interval_ms == 0 {
    280             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    281                 "poll_interval_ms",
    282             ));
    283         }
    284         if self.max_response_bytes == 0 {
    285             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    286                 "max_response_bytes",
    287             ));
    288         }
    289         self.auth.validate()
    290     }
    291 }
    292 
    293 fn remote_http_endpoint_url(
    294     config: &TradeValidationReceiptRemoteHttpProverConfig,
    295 ) -> Result<reqwest::Url, TradeValidationReceiptJobError> {
    296     if config.endpoint_url.trim().is_empty() {
    297         return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    298             "endpoint_url",
    299         ));
    300     }
    301     reqwest::Url::parse(config.endpoint_url.as_str())
    302         .map_err(|_| TradeValidationReceiptJobError::RemoteHttpInvalidConfig("endpoint_url"))
    303 }
    304 
    305 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    306 #[serde(tag = "mode", rename_all = "snake_case")]
    307 pub enum TradeValidationReceiptRemoteHttpAuth {
    308     NoAuth,
    309     BearerTokenEnv { env_var: String },
    310 }
    311 
    312 impl TradeValidationReceiptRemoteHttpAuth {
    313     fn validate(&self) -> Result<(), TradeValidationReceiptJobError> {
    314         match self {
    315             Self::NoAuth => Ok(()),
    316             Self::BearerTokenEnv { env_var } => {
    317                 validate_rhi_secret_env_var_name(env_var)?;
    318                 Ok(())
    319             }
    320         }
    321     }
    322 }
    323 
    324 fn validate_rhi_secret_env_var_name(env_var: &str) -> Result<&str, TradeValidationReceiptJobError> {
    325     let env_var = env_var.trim();
    326     if env_var.is_empty() || !env_var.starts_with("RHI_") {
    327         return Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
    328             "auth.env_var",
    329         ));
    330     }
    331     Ok(env_var)
    332 }
    333 
    334 fn remote_http_auth_token(
    335     config: &TradeValidationReceiptRemoteHttpProverConfig,
    336 ) -> Result<Option<String>, TradeValidationReceiptJobError> {
    337     match &config.auth {
    338         TradeValidationReceiptRemoteHttpAuth::NoAuth => Ok(None),
    339         TradeValidationReceiptRemoteHttpAuth::BearerTokenEnv { env_var } => {
    340             let env_var = validate_rhi_secret_env_var_name(env_var)?;
    341             let value = std::env::var(env_var).map_err(|_| {
    342                 TradeValidationReceiptJobError::RemoteHttpAuthTokenMissing(env_var.to_owned())
    343             })?;
    344             let token = value.trim();
    345             if token.is_empty() {
    346                 return Err(TradeValidationReceiptJobError::RemoteHttpAuthTokenMissing(
    347                     env_var.to_owned(),
    348                 ));
    349             }
    350             Ok(Some(token.to_owned()))
    351         }
    352     }
    353 }
    354 
    355 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    356 #[serde(deny_unknown_fields)]
    357 pub struct TradeValidationReceiptJobResult {
    358     pub cryptographic_proof_verified: bool,
    359     pub decision_event_id: String,
    360     pub event_set_root: String,
    361     pub listing_event_id: String,
    362     pub order_id: String,
    363     pub proof_generated: bool,
    364     pub proof_mode: RadrootsSp1TradeProofMode,
    365     pub proof_system: String,
    366     pub public_values_hash: String,
    367     pub prover_backend: TradeValidationReceiptProverBackend,
    368     pub receipt_event_id: String,
    369     pub receipt_kind: u32,
    370     pub reducer_output_root: String,
    371     pub request_event_id: String,
    372     pub sp1_execute_checked: bool,
    373     pub sp1_execute_public_values_hash: Option<String>,
    374     pub status: TradeValidationReceiptJobStatus,
    375     pub worker_role: TradeValidationReceiptWorkerRole,
    376 }
    377 
    378 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    379 #[serde(rename_all = "snake_case")]
    380 pub enum TradeValidationReceiptJobStatus {
    381     Succeeded,
    382 }
    383 
    384 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    385 #[serde(rename_all = "snake_case")]
    386 pub enum TradeValidationReceiptWorkerRole {
    387     NonAuthoritativeProver,
    388 }
    389 
    390 #[derive(Debug, Error)]
    391 pub enum TradeValidationReceiptJobError {
    392     #[error("event kind not supported")]
    393     UnsupportedKind,
    394     #[error("missing recipient tag")]
    395     MissingRecipient,
    396     #[error("invalid job request")]
    397     InvalidJobRequest,
    398     #[error("unsupported proof target")]
    399     UnsupportedProofTarget,
    400     #[error("unsupported witness version")]
    401     UnsupportedWitnessVersion,
    402     #[error("expected reducer program hash does not match canonical reducer")]
    403     ExpectedReducerProgramHashMismatch,
    404     #[error("expected protocol version does not match canonical protocol")]
    405     ExpectedProtocolVersionMismatch,
    406     #[error("unsupported proof mode")]
    407     UnsupportedProofMode,
    408     #[error("SP1 identity constraints require an SP1 proof mode")]
    409     Sp1IdentityConstraintsRequireSp1Proof,
    410     #[error("invalid listing event")]
    411     InvalidListingEvent,
    412     #[error("invalid signed event evidence")]
    413     InvalidSignedEvent,
    414     #[error("job request does not match fetched event set")]
    415     EventSetMismatch,
    416     #[error("invalid active trade event: {0}")]
    417     InvalidActiveTradeEvent(String),
    418     #[error("rhi prover backend is disabled")]
    419     ProverBackendDisabled,
    420     #[error("rhi prover backend requires proof_mode none")]
    421     ProverBackendRequiresNone,
    422     #[error("rhi prover backend requires an SP1 proof mode")]
    423     ProverBackendRequiresSp1Proof,
    424     #[error("rhi prover backend does not match configured policy")]
    425     ProverBackendPolicyMismatch,
    426     #[error("rhi prover backend {0} is unavailable in this build")]
    427     ProverBackendUnavailable(&'static str),
    428     #[error("configured SP1 identity policy is required for this prover backend")]
    429     Sp1IdentityPolicyRequired,
    430     #[error("remote_http prover config is required")]
    431     RemoteHttpConfigRequired,
    432     #[error("remote_http prover config field {0} is invalid")]
    433     RemoteHttpInvalidConfig(&'static str),
    434     #[error("remote_http bearer token environment variable {0} is missing or empty")]
    435     RemoteHttpAuthTokenMissing(String),
    436     #[error("remote_http transport error: {0}")]
    437     RemoteHttpTransport(String),
    438     #[error("remote_http response exceeded configured byte limit")]
    439     RemoteHttpResponseTooLarge,
    440     #[error("remote_http response field {0} is invalid")]
    441     RemoteHttpInvalidResponse(&'static str),
    442     #[error("remote_http terminal {status}: {reason_code}: {message}")]
    443     RemoteHttpTerminal {
    444         status: &'static str,
    445         reason_code: String,
    446         message: String,
    447     },
    448     #[error("remote_http polling timed out")]
    449     RemoteHttpTimeout,
    450     #[error("remote_http response identity field {0} did not match")]
    451     RemoteHttpIdentityMismatch(&'static str),
    452     #[error("expected SP1 program hash does not match configured policy")]
    453     ExpectedSp1ProgramHashMismatch,
    454     #[error("expected SP1 verifying key hash does not match configured policy")]
    455     ExpectedSp1VerifyingKeyHashMismatch,
    456     #[error("nostr error: {0}")]
    457     Nostr(#[from] radroots_nostr::error::RadrootsNostrError),
    458     #[error("serde error: {0}")]
    459     Serde(#[from] serde_json::Error),
    460     #[error("proof error: {0}")]
    461     Proof(#[from] RadrootsSp1TradeHostError),
    462     #[error("validation receipt error: {0}")]
    463     ValidationReceipt(#[from] RadrootsValidationReceiptError),
    464 }
    465 
    466 pub async fn handle_trade_validation_receipt_job_request(
    467     event: &RadrootsNostrEvent,
    468     keys: &RadrootsNostrKeys,
    469     client: &RadrootsNostrClient,
    470     prover_policy: &TradeValidationReceiptProverPolicy,
    471 ) -> Result<(), TradeValidationReceiptJobError> {
    472     let kind = event_kind_u32(event)?;
    473     if kind != KIND_TRADE_TRANSITION_PROOF_REQUEST {
    474         return Err(TradeValidationReceiptJobError::UnsupportedKind);
    475     }
    476 
    477     let tags = event_tags(event);
    478     if !tag_has_value(&tags, "p", &keys.public_key().to_string()) {
    479         return Err(TradeValidationReceiptJobError::MissingRecipient);
    480     }
    481 
    482     prover_policy.validate()?;
    483     if prover_policy.backend == TradeValidationReceiptProverBackend::Disabled {
    484         return Err(TradeValidationReceiptJobError::ProverBackendDisabled);
    485     }
    486     let request: TradeValidationReceiptJobRequest = serde_json::from_str(&event.content)?;
    487     validate_job_request_shape(&request)?;
    488     prover_policy.validate_request(&request)?;
    489 
    490     let listing_event = fetch_event_by_id_io(client, &request.listing_event_id).await?;
    491     let order_request_event = fetch_event_by_id_io(client, &request.request_event_id).await?;
    492     let order_decision_event = fetch_event_by_id_io(client, &request.decision_event_id).await?;
    493     validate_fetched_event(&listing_event, &request.listing_event_id)?;
    494     validate_fetched_event(&order_request_event, &request.request_event_id)?;
    495     validate_fetched_event(&order_decision_event, &request.decision_event_id)?;
    496 
    497     let listing_kind = event_kind_u32(&listing_event)
    498         .map_err(|_| TradeValidationReceiptJobError::InvalidListingEvent)?;
    499     if !is_listing_kind(listing_kind) {
    500         return Err(TradeValidationReceiptJobError::InvalidListingEvent);
    501     }
    502 
    503     let request_rr = radroots_event_from_nostr(&order_request_event);
    504     let decision_rr = radroots_event_from_nostr(&order_decision_event);
    505 
    506     let request_envelope = order_request_from_event(&request_rr).map_err(|error| {
    507         TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
    508     })?;
    509     let decision_envelope = order_decision_from_event(&decision_rr).map_err(|error| {
    510         TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
    511     })?;
    512 
    513     let listing_event_ptr = parse_order_listing_event_tag(&request_rr.tags)
    514         .map_err(|error| {
    515             TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
    516         })?
    517         .ok_or(TradeValidationReceiptJobError::EventSetMismatch)?;
    518     if listing_event_ptr.id != request.listing_event_id {
    519         return Err(TradeValidationReceiptJobError::EventSetMismatch);
    520     }
    521 
    522     let root_event_id = parse_order_root_tag(&decision_rr.tags).map_err(|error| {
    523         TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
    524     })?;
    525     let prev_event_id = parse_order_prev_tag(&decision_rr.tags).map_err(|error| {
    526         TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
    527     })?;
    528     if root_event_id.as_deref() != Some(request.request_event_id.as_str())
    529         || prev_event_id.as_deref() != Some(request.request_event_id.as_str())
    530     {
    531         return Err(TradeValidationReceiptJobError::EventSetMismatch);
    532     }
    533 
    534     let witness = RadrootsSp1TradeOrderAcceptanceWitness {
    535         witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION,
    536         proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(),
    537         listing_event_id: request.listing_event_id.clone(),
    538         request_event_id: request.request_event_id.clone(),
    539         decision_event_id: request.decision_event_id.clone(),
    540         event_evidence: canonical_event_evidence_from_events(
    541             &listing_event,
    542             &order_request_event,
    543             &order_decision_event,
    544         )?,
    545         request: order_request_witness_from_payload(request_envelope.payload),
    546         decision: order_decision_witness_from_payload(decision_envelope.payload),
    547         inventory_bins: request.inventory_bins.clone(),
    548         inventory_sequence: request.inventory_sequence,
    549         previous_state_root: request.previous_state_root.clone(),
    550         reducer_program_hash: request.reducer_program_hash.clone(),
    551         radroots_protocol_version: request.radroots_protocol_version.clone(),
    552         sp1_program_hash: request.sp1_program_hash.clone(),
    553         sp1_verifying_key_hash: request.sp1_verifying_key_hash.clone(),
    554     };
    555     let proof_outcome = proof_bundle_for_policy(&witness, prover_policy).await?;
    556     verify_order_acceptance_proof_artifact_structure(
    557         &proof_outcome.bundle.execution,
    558         &proof_outcome.bundle.proof,
    559     )?;
    560     let receipt = validation_receipt_for_order_acceptance_proof(&proof_outcome.bundle)?;
    561     let receipt_parts = validation_receipt_event_build(&witness.request.order_id, &receipt)?;
    562     let verified_receipt = verify_validation_receipt_event(
    563         &radroots_events::RadrootsNostrEvent {
    564             id: zero_event_id(),
    565             author: keys.public_key().to_string(),
    566             created_at: 0,
    567             kind: receipt_parts.kind,
    568             tags: receipt_parts.tags.clone(),
    569             content: receipt_parts.content.clone(),
    570             sig: zero_signature(),
    571         },
    572         RadrootsValidationReceiptExpectedBinding {
    573             event_set_root: Some(&receipt.event_set_root),
    574             listing_event_id: Some(&request.listing_event_id),
    575             order_id: Some(&witness.request.order_id),
    576             program_hash: prover_policy.expected_sp1_program_hash.as_deref(),
    577             proof_system: Some(receipt.proof.system),
    578             public_values_hash: Some(&receipt.public_values_hash),
    579             reducer_output_root: Some(&receipt.new_state_root),
    580             verifying_key_hash: prover_policy.expected_sp1_verifying_key_hash.as_deref(),
    581         },
    582     )?;
    583     let receipt_event_id = publish_event_parts_io(
    584         client,
    585         receipt_parts.kind,
    586         receipt_parts.content,
    587         receipt_parts.tags,
    588     )
    589     .await?;
    590 
    591     let result = TradeValidationReceiptJobResult {
    592         cryptographic_proof_verified: proof_outcome.cryptographic_proof_verified,
    593         decision_event_id: request.decision_event_id,
    594         event_set_root: verified_receipt.receipt.event_set_root,
    595         listing_event_id: request.listing_event_id,
    596         order_id: witness.request.order_id,
    597         proof_generated: proof_outcome.proof_generated,
    598         proof_mode: prover_policy.proof_mode,
    599         proof_system: verified_receipt.receipt.proof.system.as_str().to_string(),
    600         public_values_hash: verified_receipt.receipt.public_values_hash,
    601         prover_backend: prover_policy.backend,
    602         receipt_event_id: receipt_event_id.clone(),
    603         receipt_kind: KIND_TRADE_VALIDATION_RECEIPT,
    604         reducer_output_root: verified_receipt.receipt.new_state_root,
    605         request_event_id: request.request_event_id,
    606         sp1_execute_checked: proof_outcome.sp1_execute_checked,
    607         sp1_execute_public_values_hash: proof_outcome.sp1_execute_public_values_hash,
    608         status: TradeValidationReceiptJobStatus::Succeeded,
    609         worker_role: TradeValidationReceiptWorkerRole::NonAuthoritativeProver,
    610     };
    611     let result_content = serde_json::to_string(&result)?;
    612     let result_tags = result_tags(event, &receipt_event_id, &result);
    613     publish_event_parts_io(
    614         client,
    615         KIND_TRADE_TRANSITION_PROOF_RESULT,
    616         result_content,
    617         result_tags,
    618     )
    619     .await?;
    620 
    621     Ok(())
    622 }
    623 
    624 fn canonical_event_evidence_from_events(
    625     listing_event: &RadrootsNostrEvent,
    626     order_request_event: &RadrootsNostrEvent,
    627     order_decision_event: &RadrootsNostrEvent,
    628 ) -> Result<Vec<RadrootsSp1TradeCanonicalEventEvidence>, TradeValidationReceiptJobError> {
    629     Ok(vec![
    630         canonical_event_evidence(
    631             listing_event,
    632             RadrootsSp1TradeEventEvidenceRole::Seller,
    633             RadrootsSp1TradeEventWorkflowPosition::Listing,
    634             "001:listing",
    635         )?,
    636         canonical_event_evidence(
    637             order_request_event,
    638             RadrootsSp1TradeEventEvidenceRole::Buyer,
    639             RadrootsSp1TradeEventWorkflowPosition::OrderRequest,
    640             "002:order_request",
    641         )?,
    642         canonical_event_evidence(
    643             order_decision_event,
    644             RadrootsSp1TradeEventEvidenceRole::Seller,
    645             RadrootsSp1TradeEventWorkflowPosition::OrderDecision,
    646             "003:order_decision",
    647         )?,
    648     ])
    649 }
    650 
    651 fn canonical_event_evidence(
    652     event: &RadrootsNostrEvent,
    653     role: RadrootsSp1TradeEventEvidenceRole,
    654     workflow_position: RadrootsSp1TradeEventWorkflowPosition,
    655     ordering_key: &'static str,
    656 ) -> Result<RadrootsSp1TradeCanonicalEventEvidence, TradeValidationReceiptJobError> {
    657     event
    658         .verify()
    659         .map_err(|_| TradeValidationReceiptJobError::InvalidSignedEvent)?;
    660     let canonical_event_json = serde_json::to_string(event)?;
    661     let tags_json = serde_json::to_vec(&event.tags)?;
    662     Ok(RadrootsSp1TradeCanonicalEventEvidence {
    663         event_id: event.id.to_hex(),
    664         signer_pubkey: event.pubkey.to_hex(),
    665         kind: event_kind_u32(event)?,
    666         canonical_event_hash: hash_bytes(
    667             "radroots:canonical-event:v1",
    668             canonical_event_json.as_bytes(),
    669         ),
    670         signature_hash: hash_bytes(
    671             "radroots:event-signature:v1",
    672             event.sig.to_string().as_bytes(),
    673         ),
    674         preverified_signature: true,
    675         role,
    676         workflow_position,
    677         content_hash: hash_bytes("radroots:event-content:v1", event.content.as_bytes()),
    678         tags_hash: hash_bytes("radroots:event-tags:v1", &tags_json),
    679         ordering_key: ordering_key.to_string(),
    680     })
    681 }
    682 
    683 fn validate_fetched_event(
    684     event: &RadrootsNostrEvent,
    685     expected_event_id: &str,
    686 ) -> Result<(), TradeValidationReceiptJobError> {
    687     if event.id.to_hex() != expected_event_id {
    688         return Err(TradeValidationReceiptJobError::EventSetMismatch);
    689     }
    690     event
    691         .verify()
    692         .map_err(|_| TradeValidationReceiptJobError::InvalidSignedEvent)
    693 }
    694 
    695 fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String {
    696     let mut hasher = Sha256::new();
    697     hasher.update(domain.as_bytes());
    698     hasher.update(bytes);
    699     format!("0x{}", hex_lower(hasher.finalize().as_slice()))
    700 }
    701 
    702 fn hex_lower(bytes: &[u8]) -> String {
    703     const HEX: &[u8; 16] = b"0123456789abcdef";
    704     let mut out = String::with_capacity(bytes.len() * 2);
    705     for byte in bytes {
    706         out.push(HEX[(byte >> 4) as usize] as char);
    707         out.push(HEX[(byte & 0x0f) as usize] as char);
    708     }
    709     out
    710 }
    711 
    712 fn order_request_witness_from_payload(
    713     payload: RadrootsOrderRequest,
    714 ) -> RadrootsSp1TradeOrderRequestWitness {
    715     RadrootsSp1TradeOrderRequestWitness {
    716         order_id: payload.order_id.to_string(),
    717         listing_addr: payload.listing_addr.to_string(),
    718         buyer_pubkey: payload.buyer_pubkey.to_string(),
    719         seller_pubkey: payload.seller_pubkey.to_string(),
    720         items: payload
    721             .items
    722             .into_iter()
    723             .map(|item| RadrootsSp1TradeOrderItemWitness {
    724                 bin_id: item.bin_id.to_string(),
    725                 bin_count: item.bin_count,
    726             })
    727             .collect(),
    728     }
    729 }
    730 
    731 fn order_decision_witness_from_payload(
    732     payload: RadrootsOrderDecision,
    733 ) -> RadrootsSp1TradeOrderDecisionEventWitness {
    734     RadrootsSp1TradeOrderDecisionEventWitness {
    735         order_id: payload.order_id.to_string(),
    736         listing_addr: payload.listing_addr.to_string(),
    737         buyer_pubkey: payload.buyer_pubkey.to_string(),
    738         seller_pubkey: payload.seller_pubkey.to_string(),
    739         decision: match payload.decision {
    740             RadrootsOrderDecisionOutcome::Accepted {
    741                 inventory_commitments,
    742             } => RadrootsSp1TradeOrderDecisionWitness::Accepted {
    743                 inventory_commitments: inventory_commitments
    744                     .into_iter()
    745                     .map(|commitment| RadrootsSp1TradeInventoryCommitmentWitness {
    746                         bin_id: commitment.bin_id.to_string(),
    747                         bin_count: commitment.bin_count,
    748                     })
    749                     .collect(),
    750             },
    751             RadrootsOrderDecisionOutcome::Declined { reason } => {
    752                 RadrootsSp1TradeOrderDecisionWitness::Declined { reason }
    753             }
    754         },
    755     }
    756 }
    757 
    758 struct TradeValidationReceiptProofOutcome {
    759     bundle: RadrootsSp1TradeProofBundle,
    760     proof_generated: bool,
    761     sp1_execute_checked: bool,
    762     sp1_execute_public_values_hash: Option<String>,
    763     cryptographic_proof_verified: bool,
    764 }
    765 
    766 async fn proof_bundle_for_policy(
    767     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    768     policy: &TradeValidationReceiptProverPolicy,
    769 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    770     match policy.backend {
    771         TradeValidationReceiptProverBackend::Disabled => {
    772             Err(TradeValidationReceiptJobError::ProverBackendDisabled)
    773         }
    774         TradeValidationReceiptProverBackend::DeterministicNone => {
    775             let bundle = generate_order_acceptance_proof(witness, policy.proof_mode)?;
    776             Ok(TradeValidationReceiptProofOutcome {
    777                 bundle,
    778                 proof_generated: false,
    779                 sp1_execute_checked: false,
    780                 sp1_execute_public_values_hash: None,
    781                 cryptographic_proof_verified: false,
    782             })
    783         }
    784         TradeValidationReceiptProverBackend::LocalExecute => {
    785             run_local_execute_backend(witness, policy.proof_mode).await
    786         }
    787         TradeValidationReceiptProverBackend::LocalCpuProve => {
    788             run_local_cpu_prove_backend(witness, policy.proof_mode).await
    789         }
    790         TradeValidationReceiptProverBackend::LocalCudaProve => Err(
    791             TradeValidationReceiptJobError::ProverBackendUnavailable(policy.backend.as_str()),
    792         ),
    793         TradeValidationReceiptProverBackend::RemoteHttpProve => {
    794             run_remote_http_prove_backend(witness, policy).await
    795         }
    796     }
    797 }
    798 
    799 #[cfg(feature = "sp1_proving")]
    800 async fn run_local_execute_backend(
    801     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    802     proof_mode: RadrootsSp1TradeProofMode,
    803 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    804     let sp1_execution =
    805         radroots_sp1_host_trade::execute_order_acceptance_sp1_public_values(witness).await?;
    806     let bundle = generate_order_acceptance_proof(witness, proof_mode)?;
    807     Ok(TradeValidationReceiptProofOutcome {
    808         bundle,
    809         proof_generated: false,
    810         sp1_execute_checked: true,
    811         sp1_execute_public_values_hash: Some(sp1_execution.execution.public_values_hash),
    812         cryptographic_proof_verified: false,
    813     })
    814 }
    815 
    816 #[cfg(not(feature = "sp1_proving"))]
    817 async fn run_local_execute_backend(
    818     _witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    819     _proof_mode: RadrootsSp1TradeProofMode,
    820 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    821     Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
    822         TradeValidationReceiptProverBackend::LocalExecute.as_str(),
    823     ))
    824 }
    825 
    826 #[cfg(feature = "sp1_proving")]
    827 async fn run_local_cpu_prove_backend(
    828     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    829     proof_mode: RadrootsSp1TradeProofMode,
    830 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    831     let bundle =
    832         radroots_sp1_host_trade::generate_order_acceptance_sp1_proof(witness, proof_mode).await?;
    833     radroots_sp1_host_trade::verify_order_acceptance_resolved_sp1_proof_artifact(
    834         &bundle.execution,
    835         &RadrootsSp1TradeResolvedProofArtifact::inline(bundle.proof.clone()),
    836     )
    837     .await?;
    838     Ok(TradeValidationReceiptProofOutcome {
    839         sp1_execute_public_values_hash: Some(bundle.execution.public_values_hash.clone()),
    840         bundle,
    841         proof_generated: true,
    842         sp1_execute_checked: true,
    843         cryptographic_proof_verified: true,
    844     })
    845 }
    846 
    847 #[cfg(feature = "sp1_verify")]
    848 async fn run_remote_http_prove_backend(
    849     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    850     policy: &TradeValidationReceiptProverPolicy,
    851 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    852     let remote_http = policy
    853         .remote_http
    854         .as_ref()
    855         .ok_or(TradeValidationReceiptJobError::RemoteHttpConfigRequired)?;
    856     let expected_sp1_program_hash = policy
    857         .expected_sp1_program_hash
    858         .as_deref()
    859         .ok_or(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired)?;
    860     let expected_sp1_verifying_key_hash = policy
    861         .expected_sp1_verifying_key_hash
    862         .as_deref()
    863         .ok_or(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired)?;
    864     let execution = radroots_sp1_host_trade::execute_order_acceptance_public_values(witness)?;
    865     let request = RadrootsSp1TradeRemoteProverRequest {
    866         schema_version: RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION,
    867         request_id: remote_http_request_id(witness)?,
    868         proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(),
    869         proof_mode: policy.proof_mode,
    870         sp1_version_line: RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(),
    871         witness: witness.clone(),
    872         expected_sp1_program_hash: expected_sp1_program_hash.to_owned(),
    873         expected_sp1_verifying_key_hash: expected_sp1_verifying_key_hash.to_owned(),
    874         expected_public_values_hash: execution.public_values_hash.clone(),
    875         expected_reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
    876         expected_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
    877         expected_witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION,
    878     };
    879     let response = remote_http_completed_response(remote_http, &request).await?;
    880     let artifact = remote_http_verified_artifact(
    881         &execution,
    882         policy,
    883         expected_sp1_program_hash,
    884         expected_sp1_verifying_key_hash,
    885         &request,
    886         response,
    887     )
    888     .await?;
    889     Ok(TradeValidationReceiptProofOutcome {
    890         sp1_execute_public_values_hash: Some(execution.public_values_hash.clone()),
    891         bundle: RadrootsSp1TradeProofBundle {
    892             execution,
    893             proof: artifact,
    894         },
    895         proof_generated: true,
    896         sp1_execute_checked: true,
    897         cryptographic_proof_verified: true,
    898     })
    899 }
    900 
    901 #[cfg(not(feature = "sp1_verify"))]
    902 async fn run_remote_http_prove_backend(
    903     _witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    904     _policy: &TradeValidationReceiptProverPolicy,
    905 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
    906     Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
    907         TradeValidationReceiptProverBackend::RemoteHttpProve.as_str(),
    908     ))
    909 }
    910 
    911 #[cfg(feature = "sp1_verify")]
    912 fn remote_http_request_id(
    913     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    914 ) -> Result<String, TradeValidationReceiptJobError> {
    915     let bytes = serde_json::to_vec(witness)?;
    916     Ok(hash_bytes("radroots:rhi-remote-proof-request:v1", &bytes))
    917 }
    918 
    919 #[cfg(feature = "sp1_verify")]
    920 async fn remote_http_completed_response(
    921     config: &TradeValidationReceiptRemoteHttpProverConfig,
    922     request: &RadrootsSp1TradeRemoteProverRequest,
    923 ) -> Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError> {
    924     let mut response =
    925         remote_http_post_json_io(config, config.endpoint_url.as_str(), request).await?;
    926     remote_http_validate_response_identity(&response, request)?;
    927     match response.status {
    928         RadrootsSp1TradeRemoteProverStatus::Completed => return Ok(response),
    929         RadrootsSp1TradeRemoteProverStatus::Failed => {
    930             return Err(remote_http_terminal_error("failed", response));
    931         }
    932         RadrootsSp1TradeRemoteProverStatus::Rejected => {
    933             return Err(remote_http_terminal_error("rejected", response));
    934         }
    935         RadrootsSp1TradeRemoteProverStatus::Accepted
    936         | RadrootsSp1TradeRemoteProverStatus::Running => {}
    937     }
    938     for _ in 0..config.max_poll_attempts {
    939         let status_url = remote_http_status_url(config, &response)?;
    940         tokio::time::sleep(Duration::from_millis(config.poll_interval_ms)).await;
    941         response = remote_http_get_json_io(config, status_url.as_str(), request).await?;
    942         remote_http_validate_response_identity(&response, request)?;
    943         match response.status {
    944             RadrootsSp1TradeRemoteProverStatus::Completed => return Ok(response),
    945             RadrootsSp1TradeRemoteProverStatus::Failed => {
    946                 return Err(remote_http_terminal_error("failed", response));
    947             }
    948             RadrootsSp1TradeRemoteProverStatus::Rejected => {
    949                 return Err(remote_http_terminal_error("rejected", response));
    950             }
    951             RadrootsSp1TradeRemoteProverStatus::Accepted
    952             | RadrootsSp1TradeRemoteProverStatus::Running => {}
    953         }
    954     }
    955     Err(TradeValidationReceiptJobError::RemoteHttpTimeout)
    956 }
    957 
    958 #[cfg(feature = "sp1_verify")]
    959 fn remote_http_validate_response_identity(
    960     response: &RadrootsSp1TradeRemoteProverResponse,
    961     request: &RadrootsSp1TradeRemoteProverRequest,
    962 ) -> Result<(), TradeValidationReceiptJobError> {
    963     if response.schema_version != RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION {
    964         return Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
    965             "schema_version",
    966         ));
    967     }
    968     if response.request_id != request.request_id {
    969         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
    970             "request_id",
    971         ));
    972     }
    973     Ok(())
    974 }
    975 
    976 #[cfg(feature = "sp1_verify")]
    977 fn remote_http_terminal_error(
    978     status: &'static str,
    979     response: RadrootsSp1TradeRemoteProverResponse,
    980 ) -> TradeValidationReceiptJobError {
    981     TradeValidationReceiptJobError::RemoteHttpTerminal {
    982         status,
    983         reason_code: response
    984             .reason_code
    985             .unwrap_or_else(|| "remote_prover_terminal".to_string()),
    986         message: response
    987             .message
    988             .unwrap_or_else(|| "remote prover reached a terminal non-success state".to_string()),
    989     }
    990 }
    991 
    992 #[cfg(feature = "sp1_verify")]
    993 async fn remote_http_verified_artifact(
    994     execution: &radroots_sp1_guest_trade::RadrootsSp1TradePublicValuesExecution,
    995     policy: &TradeValidationReceiptProverPolicy,
    996     expected_sp1_program_hash: &str,
    997     expected_sp1_verifying_key_hash: &str,
    998     request: &RadrootsSp1TradeRemoteProverRequest,
    999     response: RadrootsSp1TradeRemoteProverResponse,
   1000 ) -> Result<radroots_sp1_host_trade::RadrootsSp1TradeProofArtifact, TradeValidationReceiptJobError>
   1001 {
   1002     if response.schema_version != RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION {
   1003         return Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
   1004             "schema_version",
   1005         ));
   1006     }
   1007     if response.request_id != request.request_id {
   1008         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1009             "request_id",
   1010         ));
   1011     }
   1012     if response.proof_mode != Some(policy.proof_mode) {
   1013         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1014             "proof_mode",
   1015         ));
   1016     }
   1017     if response.proof_system != Some(policy.proof_mode.proof_system()) {
   1018         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1019             "proof_system",
   1020         ));
   1021     }
   1022     if response.public_values_hash.as_deref() != Some(execution.public_values_hash.as_str()) {
   1023         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1024             "public_values_hash",
   1025         ));
   1026     }
   1027     if response.sp1_program_hash.as_deref() != Some(expected_sp1_program_hash) {
   1028         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1029             "sp1_program_hash",
   1030         ));
   1031     }
   1032     if response.sp1_verifying_key_hash.as_deref() != Some(expected_sp1_verifying_key_hash) {
   1033         return Err(TradeValidationReceiptJobError::RemoteHttpIdentityMismatch(
   1034             "sp1_verifying_key_hash",
   1035         ));
   1036     }
   1037     let artifact = response.proof_artifact.ok_or(
   1038         TradeValidationReceiptJobError::RemoteHttpInvalidResponse("proof_artifact"),
   1039     )?;
   1040     let resolved = RadrootsSp1TradeResolvedProofArtifact {
   1041         artifact,
   1042         resolved_proof_envelope_base64: response.resolved_proof_envelope_base64,
   1043     };
   1044     verify_remote_proof_artifact_io(execution, &resolved).await?;
   1045     Ok(resolved.artifact)
   1046 }
   1047 
   1048 #[cfg(feature = "sp1_verify")]
   1049 async fn verify_remote_proof_artifact_io(
   1050     execution: &radroots_sp1_guest_trade::RadrootsSp1TradePublicValuesExecution,
   1051     resolved: &RadrootsSp1TradeResolvedProofArtifact,
   1052 ) -> Result<(), TradeValidationReceiptJobError> {
   1053     #[cfg(test)]
   1054     if let Some(result) = pop_remote_proof_verification_hook() {
   1055         return result;
   1056     }
   1057 
   1058     radroots_sp1_host_trade::verify_order_acceptance_resolved_sp1_proof_artifact(
   1059         execution, resolved,
   1060     )
   1061     .await?;
   1062     Ok(())
   1063 }
   1064 
   1065 #[cfg(feature = "sp1_verify")]
   1066 async fn remote_http_post_json_io(
   1067     config: &TradeValidationReceiptRemoteHttpProverConfig,
   1068     url: &str,
   1069     request: &RadrootsSp1TradeRemoteProverRequest,
   1070 ) -> Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError> {
   1071     #[cfg(test)]
   1072     if let Some(result) = pop_remote_http_response_hook(request) {
   1073         return result;
   1074     }
   1075 
   1076     let client = remote_http_client(config)?;
   1077     let mut builder = client.post(url).json(request);
   1078     if let Some(token) = remote_http_auth_token(config)? {
   1079         builder = builder.bearer_auth(token);
   1080     }
   1081     remote_http_response_json(config, builder.send().await).await
   1082 }
   1083 
   1084 #[cfg(feature = "sp1_verify")]
   1085 async fn remote_http_get_json_io(
   1086     config: &TradeValidationReceiptRemoteHttpProverConfig,
   1087     url: &str,
   1088     _request: &RadrootsSp1TradeRemoteProverRequest,
   1089 ) -> Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError> {
   1090     #[cfg(test)]
   1091     if let Some(result) = pop_remote_http_response_hook(_request) {
   1092         return result;
   1093     }
   1094 
   1095     let client = remote_http_client(config)?;
   1096     let mut builder = client.get(url);
   1097     if let Some(token) = remote_http_auth_token(config)? {
   1098         builder = builder.bearer_auth(token);
   1099     }
   1100     remote_http_response_json(config, builder.send().await).await
   1101 }
   1102 
   1103 #[cfg(feature = "sp1_verify")]
   1104 fn remote_http_client(
   1105     config: &TradeValidationReceiptRemoteHttpProverConfig,
   1106 ) -> Result<reqwest::Client, TradeValidationReceiptJobError> {
   1107     reqwest::Client::builder()
   1108         .timeout(Duration::from_millis(config.request_timeout_ms))
   1109         .build()
   1110         .map_err(|error| TradeValidationReceiptJobError::RemoteHttpTransport(error.to_string()))
   1111 }
   1112 
   1113 #[cfg(feature = "sp1_verify")]
   1114 async fn remote_http_response_json(
   1115     config: &TradeValidationReceiptRemoteHttpProverConfig,
   1116     response: Result<reqwest::Response, reqwest::Error>,
   1117 ) -> Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError> {
   1118     let mut response = response
   1119         .map_err(|error| TradeValidationReceiptJobError::RemoteHttpTransport(error.to_string()))?;
   1120     if !response.status().is_success() {
   1121         return Err(TradeValidationReceiptJobError::RemoteHttpTransport(
   1122             format!("http status {}", response.status().as_u16()),
   1123         ));
   1124     }
   1125     if response
   1126         .content_length()
   1127         .is_some_and(|length| length > config.max_response_bytes as u64)
   1128     {
   1129         return Err(TradeValidationReceiptJobError::RemoteHttpResponseTooLarge);
   1130     }
   1131     let mut bytes = Vec::with_capacity(config.max_response_bytes.min(8192));
   1132     while let Some(chunk) = response
   1133         .chunk()
   1134         .await
   1135         .map_err(|error| TradeValidationReceiptJobError::RemoteHttpTransport(error.to_string()))?
   1136     {
   1137         if chunk.len() > config.max_response_bytes.saturating_sub(bytes.len()) {
   1138             return Err(TradeValidationReceiptJobError::RemoteHttpResponseTooLarge);
   1139         }
   1140         bytes.extend_from_slice(&chunk);
   1141     }
   1142     serde_json::from_slice::<RadrootsSp1TradeRemoteProverResponse>(&bytes)
   1143         .map_err(TradeValidationReceiptJobError::Serde)
   1144 }
   1145 
   1146 #[cfg(feature = "sp1_verify")]
   1147 fn remote_http_status_url(
   1148     config: &TradeValidationReceiptRemoteHttpProverConfig,
   1149     response: &RadrootsSp1TradeRemoteProverResponse,
   1150 ) -> Result<String, TradeValidationReceiptJobError> {
   1151     let base = remote_http_endpoint_url(config)?;
   1152     if let Some(url) = response.status_url.as_deref() {
   1153         let parsed = reqwest::Url::parse(url)
   1154             .map_err(|_| TradeValidationReceiptJobError::RemoteHttpInvalidResponse("status_url"))?;
   1155         if (parsed.scheme() != "http" && parsed.scheme() != "https")
   1156             || !remote_http_same_origin(&base, &parsed)
   1157         {
   1158             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
   1159                 "status_url",
   1160             ));
   1161         }
   1162         return Ok(parsed.to_string());
   1163     }
   1164     if let Some(path) = response.status_path.as_deref() {
   1165         if path.trim() != path
   1166             || !path.starts_with('/')
   1167             || path.starts_with("//")
   1168             || reqwest::Url::parse(path).is_ok()
   1169         {
   1170             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
   1171                 "status_path",
   1172             ));
   1173         }
   1174         let parsed = base.join(path).map_err(|_| {
   1175             TradeValidationReceiptJobError::RemoteHttpInvalidResponse("status_path")
   1176         })?;
   1177         if !remote_http_same_origin(&base, &parsed) {
   1178             return Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
   1179                 "status_path",
   1180             ));
   1181         }
   1182         return Ok(parsed.to_string());
   1183     }
   1184     Err(TradeValidationReceiptJobError::RemoteHttpInvalidResponse(
   1185         "status_url",
   1186     ))
   1187 }
   1188 
   1189 #[cfg(feature = "sp1_verify")]
   1190 fn remote_http_same_origin(base: &reqwest::Url, candidate: &reqwest::Url) -> bool {
   1191     base.scheme() == candidate.scheme()
   1192         && base.host_str() == candidate.host_str()
   1193         && base.port_or_known_default() == candidate.port_or_known_default()
   1194 }
   1195 
   1196 #[cfg(not(feature = "sp1_proving"))]
   1197 async fn run_local_cpu_prove_backend(
   1198     _witness: &RadrootsSp1TradeOrderAcceptanceWitness,
   1199     _proof_mode: RadrootsSp1TradeProofMode,
   1200 ) -> Result<TradeValidationReceiptProofOutcome, TradeValidationReceiptJobError> {
   1201     Err(TradeValidationReceiptJobError::ProverBackendUnavailable(
   1202         TradeValidationReceiptProverBackend::LocalCpuProve.as_str(),
   1203     ))
   1204 }
   1205 
   1206 fn validate_job_request_shape(
   1207     request: &TradeValidationReceiptJobRequest,
   1208 ) -> Result<(), TradeValidationReceiptJobError> {
   1209     if request.listing_event_id.trim().is_empty()
   1210         || request.request_event_id.trim().is_empty()
   1211         || request.decision_event_id.trim().is_empty()
   1212         || request.proof_target.trim().is_empty()
   1213         || request.reducer_program_hash.trim().is_empty()
   1214         || request.radroots_protocol_version.trim().is_empty()
   1215         || request.inventory_bins.is_empty()
   1216     {
   1217         return Err(TradeValidationReceiptJobError::InvalidJobRequest);
   1218     }
   1219     if request.witness_version != RADROOTS_SP1_TRADE_WITNESS_VERSION {
   1220         return Err(TradeValidationReceiptJobError::UnsupportedWitnessVersion);
   1221     }
   1222     if request.proof_target != RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET {
   1223         return Err(TradeValidationReceiptJobError::UnsupportedProofTarget);
   1224     }
   1225     if request.reducer_program_hash != RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH {
   1226         return Err(TradeValidationReceiptJobError::ExpectedReducerProgramHashMismatch);
   1227     }
   1228     if request.radroots_protocol_version != RADROOTS_SP1_TRADE_PROTOCOL_VERSION {
   1229         return Err(TradeValidationReceiptJobError::ExpectedProtocolVersionMismatch);
   1230     }
   1231     validate_optional_hash32(&request.sp1_program_hash)?;
   1232     validate_optional_hash32(&request.sp1_verifying_key_hash)?;
   1233     Ok(())
   1234 }
   1235 
   1236 fn validate_optional_hash32(value: &Option<String>) -> Result<(), TradeValidationReceiptJobError> {
   1237     if let Some(value) = value {
   1238         let hash = value.as_str();
   1239         if hash.len() != 66 || !hash.starts_with("0x") || !is_lower_hex(&hash[2..]) {
   1240             return Err(TradeValidationReceiptJobError::InvalidJobRequest);
   1241         }
   1242     }
   1243     Ok(())
   1244 }
   1245 
   1246 fn is_lower_hex(value: &str) -> bool {
   1247     value
   1248         .bytes()
   1249         .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
   1250 }
   1251 
   1252 fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeValidationReceiptJobError> {
   1253     match event.kind {
   1254         RadrootsNostrKind::Custom(value) => Ok(u32::from(value)),
   1255         _ => Err(TradeValidationReceiptJobError::UnsupportedKind),
   1256     }
   1257 }
   1258 
   1259 fn event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> {
   1260     event
   1261         .tags
   1262         .iter()
   1263         .map(|tag| tag.as_slice().to_vec())
   1264         .collect()
   1265 }
   1266 
   1267 fn result_tags(
   1268     request_event: &RadrootsNostrEvent,
   1269     receipt_event_id: &str,
   1270     result: &TradeValidationReceiptJobResult,
   1271 ) -> Vec<Vec<String>> {
   1272     vec![
   1273         vec!["p".to_string(), request_event.pubkey.to_string()],
   1274         vec![
   1275             "e".to_string(),
   1276             request_event.id.to_hex(),
   1277             String::new(),
   1278             String::new(),
   1279             "request".to_string(),
   1280         ],
   1281         vec![
   1282             "e".to_string(),
   1283             receipt_event_id.to_string(),
   1284             String::new(),
   1285             String::new(),
   1286             "receipt".to_string(),
   1287         ],
   1288         vec![
   1289             "public_values_hash".to_string(),
   1290             result.public_values_hash.clone(),
   1291         ],
   1292         vec!["proof_system".to_string(), result.proof_system.clone()],
   1293         vec![
   1294             "prover_backend".to_string(),
   1295             result.prover_backend.as_str().to_string(),
   1296         ],
   1297         vec![
   1298             "proof_mode".to_string(),
   1299             result.proof_mode.mode_label().unwrap_or("none").to_string(),
   1300         ],
   1301         vec![
   1302             "proof_generated".to_string(),
   1303             result.proof_generated.to_string(),
   1304         ],
   1305         vec![
   1306             "sp1_execute_checked".to_string(),
   1307             result.sp1_execute_checked.to_string(),
   1308         ],
   1309         vec![
   1310             "cryptographic_proof_verified".to_string(),
   1311             result.cryptographic_proof_verified.to_string(),
   1312         ],
   1313     ]
   1314 }
   1315 
   1316 fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool {
   1317     tags.iter().any(|tag| {
   1318         tag.first().map(|tag_key| tag_key.as_str()) == Some(key)
   1319             && tag.get(1).map(|tag_value| tag_value.as_str()) == Some(value)
   1320     })
   1321 }
   1322 
   1323 async fn fetch_event_by_id_io(
   1324     client: &RadrootsNostrClient,
   1325     event_id: &str,
   1326 ) -> Result<RadrootsNostrEvent, TradeValidationReceiptJobError> {
   1327     #[cfg(test)]
   1328     if let Some(result) = pop_fetch_event_by_id_hook() {
   1329         return result;
   1330     }
   1331 
   1332     Ok(radroots_nostr_fetch_event_by_id(client, event_id).await?)
   1333 }
   1334 
   1335 async fn publish_event_parts_io(
   1336     client: &RadrootsNostrClient,
   1337     kind: u32,
   1338     content: String,
   1339     tags: Vec<Vec<String>>,
   1340 ) -> Result<String, TradeValidationReceiptJobError> {
   1341     #[cfg(test)]
   1342     if let Some(result) = pop_publish_event_hook(kind, content.clone(), tags.clone()) {
   1343         return result;
   1344     }
   1345 
   1346     let builder: RadrootsNostrEventBuilder = radroots_nostr_build_event(kind, content, tags)?;
   1347     let output = radroots_nostr_send_event(client, builder).await?;
   1348     Ok(output.val.to_hex())
   1349 }
   1350 
   1351 fn zero_event_id() -> String {
   1352     "0000000000000000000000000000000000000000000000000000000000000000".to_string()
   1353 }
   1354 
   1355 fn zero_signature() -> String {
   1356     "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string()
   1357 }
   1358 
   1359 #[cfg(test)]
   1360 #[derive(Clone, Debug, PartialEq, Eq)]
   1361 struct PublishedEventParts {
   1362     kind: u32,
   1363     content: String,
   1364     tags: Vec<Vec<String>>,
   1365 }
   1366 
   1367 #[cfg(test)]
   1368 #[derive(Default)]
   1369 struct TradeValidationReceiptTestHooks {
   1370     fetch_event_by_id_results:
   1371         std::collections::VecDeque<Result<RadrootsNostrEvent, TradeValidationReceiptJobError>>,
   1372     publish_event_results:
   1373         std::collections::VecDeque<Result<String, TradeValidationReceiptJobError>>,
   1374     #[cfg(feature = "sp1_verify")]
   1375     remote_http_results: std::collections::VecDeque<
   1376         Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>,
   1377     >,
   1378     #[cfg(feature = "sp1_verify")]
   1379     remote_http_requests: Vec<RadrootsSp1TradeRemoteProverRequest>,
   1380     #[cfg(feature = "sp1_verify")]
   1381     remote_proof_verification_results:
   1382         std::collections::VecDeque<Result<(), TradeValidationReceiptJobError>>,
   1383     published_events: Vec<PublishedEventParts>,
   1384 }
   1385 
   1386 #[cfg(test)]
   1387 static TRADE_VALIDATION_RECEIPT_TEST_HOOKS: std::sync::OnceLock<
   1388     std::sync::Mutex<TradeValidationReceiptTestHooks>,
   1389 > = std::sync::OnceLock::new();
   1390 
   1391 #[cfg(test)]
   1392 fn trade_validation_receipt_test_hooks()
   1393 -> &'static std::sync::Mutex<TradeValidationReceiptTestHooks> {
   1394     TRADE_VALIDATION_RECEIPT_TEST_HOOKS
   1395         .get_or_init(|| std::sync::Mutex::new(TradeValidationReceiptTestHooks::default()))
   1396 }
   1397 
   1398 #[cfg(test)]
   1399 fn pop_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeValidationReceiptJobError>>
   1400 {
   1401     trade_validation_receipt_test_hooks()
   1402         .lock()
   1403         .unwrap_or_else(std::sync::PoisonError::into_inner)
   1404         .fetch_event_by_id_results
   1405         .pop_front()
   1406 }
   1407 
   1408 #[cfg(test)]
   1409 fn pop_publish_event_hook(
   1410     kind: u32,
   1411     content: String,
   1412     tags: Vec<Vec<String>>,
   1413 ) -> Option<Result<String, TradeValidationReceiptJobError>> {
   1414     let mut hooks = trade_validation_receipt_test_hooks()
   1415         .lock()
   1416         .unwrap_or_else(std::sync::PoisonError::into_inner);
   1417     hooks.published_events.push(PublishedEventParts {
   1418         kind,
   1419         content,
   1420         tags,
   1421     });
   1422     hooks.publish_event_results.pop_front()
   1423 }
   1424 
   1425 #[cfg(all(test, feature = "sp1_verify"))]
   1426 fn pop_remote_http_response_hook(
   1427     request: &RadrootsSp1TradeRemoteProverRequest,
   1428 ) -> Option<Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>> {
   1429     trade_validation_receipt_test_hooks()
   1430         .lock()
   1431         .unwrap_or_else(std::sync::PoisonError::into_inner)
   1432         .remote_http_requests
   1433         .push(request.clone());
   1434     pop_remote_http_response_hook_without_request().map(|result| {
   1435         result.and_then(|response| remote_http_test_response_for_request(request, response))
   1436     })
   1437 }
   1438 
   1439 #[cfg(all(test, feature = "sp1_verify"))]
   1440 fn pop_remote_http_response_hook_without_request()
   1441 -> Option<Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>> {
   1442     trade_validation_receipt_test_hooks()
   1443         .lock()
   1444         .unwrap_or_else(std::sync::PoisonError::into_inner)
   1445         .remote_http_results
   1446         .pop_front()
   1447 }
   1448 
   1449 #[cfg(all(test, feature = "sp1_verify"))]
   1450 fn remote_http_test_response_for_request(
   1451     request: &RadrootsSp1TradeRemoteProverRequest,
   1452     mut response: RadrootsSp1TradeRemoteProverResponse,
   1453 ) -> Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError> {
   1454     if response.request_id == "__request_id__" {
   1455         response.request_id = request.request_id.clone();
   1456     }
   1457     if response.status == RadrootsSp1TradeRemoteProverStatus::Completed
   1458         && response.proof_artifact.is_none()
   1459     {
   1460         let execution =
   1461             radroots_sp1_host_trade::execute_order_acceptance_public_values(&request.witness)?;
   1462         let artifact =
   1463             radroots_sp1_host_trade::referenced_order_acceptance_proof_artifact_for_execution(
   1464                 &execution,
   1465                 request.proof_mode,
   1466                 format!("radroots-proof://sha256/{}", "1".repeat(64)),
   1467             )?;
   1468         if response.proof_system.is_none() {
   1469             response.proof_system = Some(request.proof_mode.proof_system());
   1470         }
   1471         if response.proof_mode.is_none() {
   1472             response.proof_mode = Some(request.proof_mode);
   1473         }
   1474         if response.public_values_hash.is_none() {
   1475             response.public_values_hash = Some(execution.public_values_hash);
   1476         }
   1477         if response.sp1_program_hash.is_none() {
   1478             response.sp1_program_hash = Some(request.expected_sp1_program_hash.clone());
   1479         }
   1480         if response.sp1_verifying_key_hash.is_none() {
   1481             response.sp1_verifying_key_hash = Some(request.expected_sp1_verifying_key_hash.clone());
   1482         }
   1483         if response.proof_artifact.is_none() {
   1484             response.proof_artifact = Some(artifact);
   1485         }
   1486     }
   1487     Ok(response)
   1488 }
   1489 
   1490 #[cfg(all(test, feature = "sp1_verify"))]
   1491 fn pop_remote_proof_verification_hook() -> Option<Result<(), TradeValidationReceiptJobError>> {
   1492     trade_validation_receipt_test_hooks()
   1493         .lock()
   1494         .unwrap_or_else(std::sync::PoisonError::into_inner)
   1495         .remote_proof_verification_results
   1496         .pop_front()
   1497 }
   1498 
   1499 #[cfg(test)]
   1500 #[cfg_attr(coverage_nightly, coverage(off))]
   1501 mod tests {
   1502     use super::{
   1503         TradeValidationReceiptJobError, TradeValidationReceiptJobRequest,
   1504         TradeValidationReceiptJobResult, TradeValidationReceiptProverBackend,
   1505         TradeValidationReceiptProverPolicy, TradeValidationReceiptRemoteHttpAuth,
   1506         TradeValidationReceiptRemoteHttpProverConfig, TradeValidationReceiptTestHooks,
   1507         handle_trade_validation_receipt_job_request, trade_validation_receipt_test_hooks,
   1508     };
   1509     use radroots_core::{
   1510         RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
   1511     };
   1512     use radroots_events::RadrootsNostrEventPtr;
   1513     use radroots_events::ids::{
   1514         RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
   1515         RadrootsOrderQuoteId, RadrootsPublicKey,
   1516     };
   1517     use radroots_events::kinds::{
   1518         KIND_LISTING, KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT,
   1519         KIND_TRADE_VALIDATION_RECEIPT,
   1520     };
   1521     use radroots_events::order::{
   1522         RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem,
   1523         RadrootsOrderEconomicLine, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
   1524         RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
   1525     };
   1526     use radroots_events_codec::order::{order_decision_event_build, order_request_event_build};
   1527     use radroots_nostr::prelude::{
   1528         RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys,
   1529         RadrootsNostrKind, RadrootsNostrTag, RadrootsNostrTagKind, radroots_event_from_nostr,
   1530         radroots_nostr_build_event,
   1531     };
   1532     use radroots_sp1_guest_trade::{
   1533         RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH,
   1534         RadrootsSp1TradeInventoryBinWitness,
   1535     };
   1536     #[cfg(feature = "sp1_verify")]
   1537     use radroots_sp1_host_trade::RadrootsSp1TradeHostError;
   1538     use radroots_sp1_host_trade::RadrootsSp1TradeProofMode;
   1539     #[cfg(feature = "sp1_verify")]
   1540     use radroots_sp1_host_trade::{
   1541         RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, RadrootsSp1TradeRemoteProverRequest,
   1542         RadrootsSp1TradeRemoteProverResponse, RadrootsSp1TradeRemoteProverStatus,
   1543     };
   1544     use radroots_trade::validation_receipt::{
   1545         RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem,
   1546         verify_validation_receipt_event,
   1547     };
   1548     use std::sync::{Mutex, MutexGuard};
   1549 
   1550     static TEST_LOCK: Mutex<()> = Mutex::new(());
   1551 
   1552     fn test_guard() -> MutexGuard<'static, ()> {
   1553         let guard = TEST_LOCK
   1554             .lock()
   1555             .unwrap_or_else(std::sync::PoisonError::into_inner);
   1556         *trade_validation_receipt_test_hooks()
   1557             .lock()
   1558             .unwrap_or_else(std::sync::PoisonError::into_inner) =
   1559             TradeValidationReceiptTestHooks::default();
   1560         guard
   1561     }
   1562 
   1563     fn publish_result_id(index: u8) -> String {
   1564         format!("{index:064x}")
   1565     }
   1566 
   1567     fn listing_addr_for_seller(seller: &RadrootsNostrKeys) -> String {
   1568         format!(
   1569             "30402:{}:AAAAAAAAAAAAAAAAAAAAAA",
   1570             seller.public_key().to_hex()
   1571         )
   1572     }
   1573 
   1574     fn typed_listing_addr(listing_addr: &str) -> RadrootsListingAddress {
   1575         RadrootsListingAddress::parse(listing_addr).expect("listing address")
   1576     }
   1577 
   1578     fn typed_order_id(order_id: &str) -> RadrootsOrderId {
   1579         RadrootsOrderId::parse(order_id).expect("order id")
   1580     }
   1581 
   1582     fn typed_quote_id(order_id: &str) -> RadrootsOrderQuoteId {
   1583         RadrootsOrderQuoteId::parse(format!("{order_id}-quote")).expect("quote id")
   1584     }
   1585 
   1586     fn typed_bin_id() -> RadrootsInventoryBinId {
   1587         RadrootsInventoryBinId::parse("bin-1").expect("bin id")
   1588     }
   1589 
   1590     fn typed_pubkey(keys: &RadrootsNostrKeys) -> RadrootsPublicKey {
   1591         RadrootsPublicKey::parse(keys.public_key().to_hex()).expect("public key")
   1592     }
   1593 
   1594     fn typed_event_id(event: &RadrootsNostrEvent) -> RadrootsEventId {
   1595         RadrootsEventId::parse(event.id.to_hex()).expect("event id")
   1596     }
   1597 
   1598     fn signed_event(
   1599         keys: &RadrootsNostrKeys,
   1600         kind: u32,
   1601         content: impl Into<String>,
   1602         tags: Vec<Vec<String>>,
   1603     ) -> RadrootsNostrEvent {
   1604         radroots_nostr_build_event(kind, content.into(), tags)
   1605             .expect("event builder")
   1606             .sign_with_keys(keys)
   1607             .expect("signed event")
   1608     }
   1609 
   1610     fn listing_event(seller: &RadrootsNostrKeys) -> RadrootsNostrEvent {
   1611         signed_event(
   1612             seller,
   1613             KIND_LISTING,
   1614             "{}",
   1615             vec![vec!["d".to_string(), "listing-1".to_string()]],
   1616         )
   1617     }
   1618 
   1619     fn request_payload(
   1620         order_id: &str,
   1621         listing_addr: &str,
   1622         buyer: &RadrootsNostrKeys,
   1623         seller: &RadrootsNostrKeys,
   1624     ) -> RadrootsOrderRequest {
   1625         RadrootsOrderRequest {
   1626             order_id: typed_order_id(order_id),
   1627             listing_addr: typed_listing_addr(listing_addr),
   1628             buyer_pubkey: typed_pubkey(buyer),
   1629             seller_pubkey: typed_pubkey(seller),
   1630             items: vec![RadrootsOrderItem {
   1631                 bin_id: typed_bin_id(),
   1632                 bin_count: 2,
   1633             }],
   1634             economics: economics(order_id, 2),
   1635         }
   1636     }
   1637 
   1638     fn decision_payload(
   1639         order_id: &str,
   1640         listing_addr: &str,
   1641         buyer: &RadrootsNostrKeys,
   1642         seller: &RadrootsNostrKeys,
   1643     ) -> RadrootsOrderDecision {
   1644         RadrootsOrderDecision {
   1645             order_id: typed_order_id(order_id),
   1646             listing_addr: typed_listing_addr(listing_addr),
   1647             buyer_pubkey: typed_pubkey(buyer),
   1648             seller_pubkey: typed_pubkey(seller),
   1649             decision: RadrootsOrderDecisionOutcome::Accepted {
   1650                 inventory_commitments: vec![RadrootsOrderInventoryCommitment {
   1651                     bin_id: typed_bin_id(),
   1652                     bin_count: 2,
   1653                 }],
   1654             },
   1655         }
   1656     }
   1657 
   1658     fn economics(order_id: &str, bin_count: u32) -> RadrootsOrderEconomics {
   1659         let subtotal = RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count);
   1660         let money = RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD);
   1661         RadrootsOrderEconomics {
   1662             quote_id: typed_quote_id(order_id),
   1663             quote_version: 1,
   1664             pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
   1665             currency: RadrootsCoreCurrency::USD,
   1666             items: vec![RadrootsOrderEconomicItem {
   1667                 bin_id: typed_bin_id(),
   1668                 bin_count,
   1669                 quantity_amount: RadrootsCoreDecimal::from(1u32),
   1670                 quantity_unit: RadrootsCoreUnit::Each,
   1671                 unit_price_amount: RadrootsCoreDecimal::from(5u32),
   1672                 unit_price_currency: RadrootsCoreCurrency::USD,
   1673                 line_subtotal: money.clone(),
   1674             }],
   1675             discounts: Vec::<RadrootsOrderEconomicLine>::new(),
   1676             adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
   1677             subtotal: money.clone(),
   1678             discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
   1679             adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
   1680             total: money,
   1681         }
   1682     }
   1683 
   1684     fn signed_order_events(
   1685         buyer: &RadrootsNostrKeys,
   1686         seller: &RadrootsNostrKeys,
   1687         listing_event: &RadrootsNostrEvent,
   1688     ) -> (RadrootsNostrEvent, RadrootsNostrEvent) {
   1689         let listing_addr = listing_addr_for_seller(seller);
   1690         let order_id = "order-1";
   1691         let listing_ptr = RadrootsNostrEventPtr {
   1692             id: listing_event.id.to_hex(),
   1693             relays: None,
   1694         };
   1695         let request_wire = order_request_event_build(
   1696             &listing_ptr,
   1697             &request_payload(order_id, &listing_addr, buyer, seller),
   1698         )
   1699         .expect("request wire");
   1700         let request_event = signed_event(
   1701             buyer,
   1702             request_wire.kind,
   1703             request_wire.content,
   1704             request_wire.tags,
   1705         );
   1706         let decision_wire = order_decision_event_build(
   1707             &typed_event_id(&request_event),
   1708             &typed_event_id(&request_event),
   1709             &decision_payload(order_id, &listing_addr, buyer, seller),
   1710         )
   1711         .expect("decision wire");
   1712         let decision_event = signed_event(
   1713             seller,
   1714             decision_wire.kind,
   1715             decision_wire.content,
   1716             decision_wire.tags,
   1717         );
   1718         (request_event, decision_event)
   1719     }
   1720 
   1721     fn job_request(
   1722         requester: &RadrootsNostrKeys,
   1723         worker: &RadrootsNostrKeys,
   1724         listing_event: &RadrootsNostrEvent,
   1725         request_event: &RadrootsNostrEvent,
   1726         decision_event: &RadrootsNostrEvent,
   1727         proof_mode: RadrootsSp1TradeProofMode,
   1728         sp1_program_hash: Option<String>,
   1729         sp1_verifying_key_hash: Option<String>,
   1730     ) -> RadrootsNostrEvent {
   1731         let request = TradeValidationReceiptJobRequest {
   1732             witness_version: radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_WITNESS_VERSION,
   1733             proof_target:
   1734                 radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET
   1735                     .to_string(),
   1736             listing_event_id: listing_event.id.to_hex(),
   1737             request_event_id: request_event.id.to_hex(),
   1738             decision_event_id: decision_event.id.to_hex(),
   1739             inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness {
   1740                 bin_id: "bin-1".to_string(),
   1741                 listing_capacity: 5,
   1742                 previous_reserved: 1,
   1743             }],
   1744             inventory_sequence: 7,
   1745             previous_state_root: None,
   1746             proof_mode,
   1747             reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
   1748             radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
   1749             sp1_program_hash,
   1750             sp1_verifying_key_hash,
   1751         };
   1752         signed_event(
   1753             requester,
   1754             KIND_TRADE_TRANSITION_PROOF_REQUEST,
   1755             serde_json::to_string(&request).expect("job json"),
   1756             vec![vec!["p".to_string(), worker.public_key().to_string()]],
   1757         )
   1758     }
   1759 
   1760     fn client_for(keys: &RadrootsNostrKeys) -> RadrootsNostrClient {
   1761         RadrootsNostrClient::new(keys.clone())
   1762     }
   1763 
   1764     fn deterministic_policy() -> TradeValidationReceiptProverPolicy {
   1765         TradeValidationReceiptProverPolicy::deterministic_none()
   1766     }
   1767 
   1768     fn hash32(ch: char) -> String {
   1769         format!("0x{}", ch.to_string().repeat(64))
   1770     }
   1771 
   1772     fn remote_http_config() -> TradeValidationReceiptRemoteHttpProverConfig {
   1773         TradeValidationReceiptRemoteHttpProverConfig {
   1774             endpoint_url: "http://127.0.0.1:65535/prove".to_string(),
   1775             auth: TradeValidationReceiptRemoteHttpAuth::NoAuth,
   1776             request_timeout_ms: 1000,
   1777             poll_interval_ms: 1,
   1778             max_poll_attempts: 1,
   1779             max_response_bytes: 65_536,
   1780         }
   1781     }
   1782 
   1783     fn remote_http_policy() -> TradeValidationReceiptProverPolicy {
   1784         TradeValidationReceiptProverPolicy {
   1785             backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
   1786             proof_mode: RadrootsSp1TradeProofMode::Core,
   1787             expected_sp1_program_hash: Some(hash32('a')),
   1788             expected_sp1_verifying_key_hash: Some(hash32('b')),
   1789             remote_http: Some(remote_http_config()),
   1790         }
   1791     }
   1792 
   1793     #[cfg(feature = "sp1_verify")]
   1794     fn remote_response(
   1795         status: RadrootsSp1TradeRemoteProverStatus,
   1796     ) -> RadrootsSp1TradeRemoteProverResponse {
   1797         RadrootsSp1TradeRemoteProverResponse {
   1798             schema_version: RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION,
   1799             request_id: "__request_id__".to_string(),
   1800             status,
   1801             status_url: None,
   1802             status_path: None,
   1803             proof_system: None,
   1804             proof_mode: None,
   1805             public_values_hash: None,
   1806             sp1_program_hash: None,
   1807             sp1_verifying_key_hash: None,
   1808             proof_artifact: None,
   1809             resolved_proof_envelope_base64: None,
   1810             reason_code: None,
   1811             message: None,
   1812             detail: None,
   1813         }
   1814     }
   1815 
   1816     #[cfg(feature = "sp1_verify")]
   1817     fn remote_http_local_response_url(response: &'static str) -> String {
   1818         let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("test listener");
   1819         let addr = listener.local_addr().expect("test listener address");
   1820         std::thread::spawn(move || {
   1821             let (mut stream, _) = listener.accept().expect("test connection");
   1822             let mut buffer = [0; 4096];
   1823             let _ = std::io::Read::read(&mut stream, &mut buffer);
   1824             std::io::Write::write_all(&mut stream, response.as_bytes()).expect("test response");
   1825         });
   1826         format!("http://{addr}/prove")
   1827     }
   1828 
   1829     #[cfg(feature = "sp1_verify")]
   1830     async fn run_remote_http_job(
   1831         remote_http_results: Vec<
   1832             Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>,
   1833         >,
   1834         remote_proof_verification_results: Vec<Result<(), TradeValidationReceiptJobError>>,
   1835         publish_results: Vec<Result<String, TradeValidationReceiptJobError>>,
   1836     ) -> Result<Vec<super::PublishedEventParts>, TradeValidationReceiptJobError> {
   1837         run_remote_http_job_with_policy(
   1838             remote_http_policy(),
   1839             remote_http_results,
   1840             remote_proof_verification_results,
   1841             publish_results,
   1842         )
   1843         .await
   1844     }
   1845 
   1846     #[cfg(feature = "sp1_verify")]
   1847     async fn run_remote_http_job_with_policy(
   1848         policy: TradeValidationReceiptProverPolicy,
   1849         remote_http_results: Vec<
   1850             Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>,
   1851         >,
   1852         remote_proof_verification_results: Vec<Result<(), TradeValidationReceiptJobError>>,
   1853         publish_results: Vec<Result<String, TradeValidationReceiptJobError>>,
   1854     ) -> Result<Vec<super::PublishedEventParts>, TradeValidationReceiptJobError> {
   1855         run_remote_http_job_with_policy_and_requests(
   1856             policy,
   1857             remote_http_results,
   1858             remote_proof_verification_results,
   1859             publish_results,
   1860         )
   1861         .await
   1862         .map(|(published, _)| published)
   1863     }
   1864 
   1865     #[cfg(feature = "sp1_verify")]
   1866     async fn run_remote_http_job_with_policy_and_requests(
   1867         policy: TradeValidationReceiptProverPolicy,
   1868         remote_http_results: Vec<
   1869             Result<RadrootsSp1TradeRemoteProverResponse, TradeValidationReceiptJobError>,
   1870         >,
   1871         remote_proof_verification_results: Vec<Result<(), TradeValidationReceiptJobError>>,
   1872         publish_results: Vec<Result<String, TradeValidationReceiptJobError>>,
   1873     ) -> Result<
   1874         (
   1875             Vec<super::PublishedEventParts>,
   1876             Vec<RadrootsSp1TradeRemoteProverRequest>,
   1877         ),
   1878         TradeValidationReceiptJobError,
   1879     > {
   1880         let _guard = test_guard();
   1881         let worker = RadrootsNostrKeys::generate();
   1882         let requester = RadrootsNostrKeys::generate();
   1883         let buyer = RadrootsNostrKeys::generate();
   1884         let seller = RadrootsNostrKeys::generate();
   1885         let listing_event = listing_event(&seller);
   1886         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   1887         let job = job_request(
   1888             &requester,
   1889             &worker,
   1890             &listing_event,
   1891             &request_event,
   1892             &decision_event,
   1893             RadrootsSp1TradeProofMode::Core,
   1894             Some(hash32('a')),
   1895             Some(hash32('b')),
   1896         );
   1897 
   1898         {
   1899             let mut hooks = trade_validation_receipt_test_hooks()
   1900                 .lock()
   1901                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   1902             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   1903             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   1904             hooks
   1905                 .fetch_event_by_id_results
   1906                 .push_back(Ok(decision_event));
   1907             hooks.remote_http_results.extend(remote_http_results);
   1908             hooks
   1909                 .remote_proof_verification_results
   1910                 .extend(remote_proof_verification_results);
   1911             hooks.publish_event_results.extend(publish_results);
   1912         }
   1913 
   1914         handle_trade_validation_receipt_job_request(&job, &worker, &client_for(&worker), &policy)
   1915             .await?;
   1916 
   1917         let hooks = trade_validation_receipt_test_hooks()
   1918             .lock()
   1919             .unwrap_or_else(std::sync::PoisonError::into_inner);
   1920         Ok((
   1921             hooks.published_events.clone(),
   1922             hooks.remote_http_requests.clone(),
   1923         ))
   1924     }
   1925 
   1926     #[test]
   1927     fn prover_policy_requires_configured_sp1_identity_for_local_cpu() {
   1928         let missing_identity = TradeValidationReceiptProverPolicy {
   1929             backend: TradeValidationReceiptProverBackend::LocalCpuProve,
   1930             proof_mode: RadrootsSp1TradeProofMode::Core,
   1931             expected_sp1_program_hash: None,
   1932             expected_sp1_verifying_key_hash: Some(hash32('b')),
   1933             remote_http: None,
   1934         };
   1935         assert!(matches!(
   1936             missing_identity.validate(),
   1937             Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired)
   1938         ));
   1939 
   1940         let policy = TradeValidationReceiptProverPolicy {
   1941             backend: TradeValidationReceiptProverBackend::LocalCpuProve,
   1942             proof_mode: RadrootsSp1TradeProofMode::Core,
   1943             expected_sp1_program_hash: Some(hash32('a')),
   1944             expected_sp1_verifying_key_hash: Some(hash32('b')),
   1945             remote_http: None,
   1946         };
   1947         let request = TradeValidationReceiptJobRequest {
   1948             witness_version: radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_WITNESS_VERSION,
   1949             proof_target:
   1950                 radroots_sp1_guest_trade::RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET
   1951                     .to_string(),
   1952             listing_event_id: "listing-event".to_string(),
   1953             request_event_id: "request-event".to_string(),
   1954             decision_event_id: "decision-event".to_string(),
   1955             inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness {
   1956                 bin_id: "bin-1".to_string(),
   1957                 listing_capacity: 5,
   1958                 previous_reserved: 1,
   1959             }],
   1960             inventory_sequence: 7,
   1961             previous_state_root: None,
   1962             proof_mode: RadrootsSp1TradeProofMode::Core,
   1963             reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
   1964             radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
   1965             sp1_program_hash: Some(hash32('c')),
   1966             sp1_verifying_key_hash: Some(hash32('b')),
   1967         };
   1968         assert!(matches!(
   1969             policy.validate_request(&request),
   1970             Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch)
   1971         ));
   1972 
   1973         let mut request = request;
   1974         request.sp1_program_hash = None;
   1975         request.sp1_verifying_key_hash = None;
   1976         assert!(matches!(
   1977             policy.validate_request(&request),
   1978             Err(TradeValidationReceiptJobError::ExpectedSp1ProgramHashMismatch)
   1979         ));
   1980     }
   1981 
   1982     #[test]
   1983     fn remote_http_policy_requires_explicit_config_and_identity_before_relay_fetch() {
   1984         let missing_config = TradeValidationReceiptProverPolicy {
   1985             backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
   1986             proof_mode: RadrootsSp1TradeProofMode::Core,
   1987             expected_sp1_program_hash: Some(hash32('a')),
   1988             expected_sp1_verifying_key_hash: Some(hash32('b')),
   1989             remote_http: None,
   1990         };
   1991         assert!(matches!(
   1992             missing_config.validate(),
   1993             Err(TradeValidationReceiptJobError::RemoteHttpConfigRequired)
   1994         ));
   1995 
   1996         let missing_identity = TradeValidationReceiptProverPolicy {
   1997             backend: TradeValidationReceiptProverBackend::RemoteHttpProve,
   1998             proof_mode: RadrootsSp1TradeProofMode::Core,
   1999             expected_sp1_program_hash: None,
   2000             expected_sp1_verifying_key_hash: Some(hash32('b')),
   2001             remote_http: Some(remote_http_config()),
   2002         };
   2003         assert!(matches!(
   2004             missing_identity.validate(),
   2005             Err(TradeValidationReceiptJobError::Sp1IdentityPolicyRequired)
   2006         ));
   2007 
   2008         let mut invalid_config = remote_http_policy();
   2009         invalid_config
   2010             .remote_http
   2011             .as_mut()
   2012             .expect("remote config")
   2013             .endpoint_url = "file:///tmp/prove".to_string();
   2014         assert!(matches!(
   2015             invalid_config.validate(),
   2016             Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
   2017                 "endpoint_url"
   2018             ))
   2019         ));
   2020 
   2021         let mut bearer_over_http = remote_http_policy();
   2022         bearer_over_http
   2023             .remote_http
   2024             .as_mut()
   2025             .expect("remote config")
   2026             .auth = TradeValidationReceiptRemoteHttpAuth::BearerTokenEnv {
   2027             env_var: "RHI_TEST_REMOTE_HTTP_TOKEN".to_string(),
   2028         };
   2029         assert!(matches!(
   2030             bearer_over_http.validate(),
   2031             Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
   2032                 "auth.endpoint_url_scheme"
   2033             ))
   2034         ));
   2035     }
   2036 
   2037     #[test]
   2038     fn remote_http_auth_env_var_must_use_rhi_prefix_before_process_env_read() {
   2039         let mut policy = remote_http_policy();
   2040         let remote_http = policy.remote_http.as_mut().expect("remote config");
   2041         remote_http.endpoint_url = "https://example.test/prove".to_string();
   2042         remote_http.auth = TradeValidationReceiptRemoteHttpAuth::BearerTokenEnv {
   2043             env_var: "RADROOTS_TEST_REMOTE_HTTP_TOKEN".to_string(),
   2044         };
   2045 
   2046         assert!(matches!(
   2047             policy.validate(),
   2048             Err(TradeValidationReceiptJobError::RemoteHttpInvalidConfig(
   2049                 "auth.env_var"
   2050             ))
   2051         ));
   2052     }
   2053 
   2054     #[cfg(feature = "sp1_verify")]
   2055     #[test]
   2056     fn remote_http_policy_accepts_core_mode_when_configured() {
   2057         assert!(remote_http_policy().validate().is_ok());
   2058     }
   2059 
   2060     #[test]
   2061     fn remote_http_policy_rejects_non_core_sp1_mode_before_remote_work() {
   2062         let mut policy = remote_http_policy();
   2063         policy.proof_mode = RadrootsSp1TradeProofMode::Compressed;
   2064 
   2065         assert!(matches!(
   2066             policy.validate(),
   2067             Err(TradeValidationReceiptJobError::UnsupportedProofMode)
   2068         ));
   2069     }
   2070 
   2071     #[cfg(feature = "sp1_verify")]
   2072     #[tokio::test]
   2073     async fn remote_http_prove_publishes_only_after_remote_artifact_verification() {
   2074         let (published, requests) = run_remote_http_job_with_policy_and_requests(
   2075             remote_http_policy(),
   2076             vec![Ok(remote_response(
   2077                 RadrootsSp1TradeRemoteProverStatus::Completed,
   2078             ))],
   2079             vec![Ok(())],
   2080             vec![Ok(publish_result_id(1)), Ok(publish_result_id(2))],
   2081         )
   2082         .await
   2083         .expect("remote proof job");
   2084 
   2085         assert_eq!(published.len(), 2);
   2086         assert_eq!(published[0].kind, KIND_TRADE_VALIDATION_RECEIPT);
   2087         assert_eq!(published[1].kind, KIND_TRADE_TRANSITION_PROOF_RESULT);
   2088         let result: TradeValidationReceiptJobResult =
   2089             serde_json::from_str(&published[1].content).expect("result json");
   2090         assert_eq!(
   2091             result.prover_backend,
   2092             TradeValidationReceiptProverBackend::RemoteHttpProve
   2093         );
   2094         assert!(result.proof_generated);
   2095         assert_eq!(result.proof_mode, RadrootsSp1TradeProofMode::Core);
   2096         assert_eq!(result.proof_system, "sp1_core");
   2097         assert!(result.sp1_execute_checked);
   2098         assert_eq!(
   2099             result.sp1_execute_public_values_hash.as_deref(),
   2100             Some(result.public_values_hash.as_str())
   2101         );
   2102         assert_eq!(requests.len(), 1);
   2103         assert_eq!(
   2104             requests[0].expected_public_values_hash,
   2105             result.public_values_hash
   2106         );
   2107         assert!(result.cryptographic_proof_verified);
   2108     }
   2109 
   2110     #[cfg(feature = "sp1_verify")]
   2111     #[tokio::test]
   2112     async fn remote_http_prove_polls_running_until_completed() {
   2113         let mut policy = remote_http_policy();
   2114         policy
   2115             .remote_http
   2116             .as_mut()
   2117             .expect("remote config")
   2118             .max_poll_attempts = 2;
   2119         let mut accepted = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2120         accepted.status_path = Some("/prove/status/request-1".to_string());
   2121         let mut running = remote_response(RadrootsSp1TradeRemoteProverStatus::Running);
   2122         running.status_path = Some("/prove/status/request-1".to_string());
   2123         let published = run_remote_http_job_with_policy(
   2124             policy,
   2125             vec![
   2126                 Ok(accepted),
   2127                 Ok(running),
   2128                 Ok(remote_response(
   2129                     RadrootsSp1TradeRemoteProverStatus::Completed,
   2130                 )),
   2131             ],
   2132             vec![Ok(())],
   2133             vec![Ok(publish_result_id(1)), Ok(publish_result_id(2))],
   2134         )
   2135         .await
   2136         .expect("polled remote proof job");
   2137 
   2138         assert_eq!(published.len(), 2);
   2139         let result: TradeValidationReceiptJobResult =
   2140             serde_json::from_str(&published[1].content).expect("result json");
   2141         assert_eq!(
   2142             result.prover_backend,
   2143             TradeValidationReceiptProverBackend::RemoteHttpProve
   2144         );
   2145         assert!(result.cryptographic_proof_verified);
   2146     }
   2147 
   2148     #[cfg(feature = "sp1_verify")]
   2149     #[tokio::test]
   2150     async fn remote_http_prove_accepts_same_origin_status_url() {
   2151         let mut policy = remote_http_policy();
   2152         policy
   2153             .remote_http
   2154             .as_mut()
   2155             .expect("remote config")
   2156             .max_poll_attempts = 1;
   2157         let mut accepted = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2158         accepted.status_url = Some("http://127.0.0.1:65535/prove/status/request-1".to_string());
   2159         let published = run_remote_http_job_with_policy(
   2160             policy,
   2161             vec![
   2162                 Ok(accepted),
   2163                 Ok(remote_response(
   2164                     RadrootsSp1TradeRemoteProverStatus::Completed,
   2165                 )),
   2166             ],
   2167             vec![Ok(())],
   2168             vec![Ok(publish_result_id(1)), Ok(publish_result_id(2))],
   2169         )
   2170         .await
   2171         .expect("same-origin status url");
   2172 
   2173         assert_eq!(published.len(), 2);
   2174     }
   2175 
   2176     #[cfg(feature = "sp1_verify")]
   2177     #[tokio::test]
   2178     async fn remote_http_prove_rejects_cross_origin_status_url() {
   2179         let mut accepted = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2180         accepted.status_url = Some("http://127.0.0.2:65535/prove/status/request-1".to_string());
   2181         let error = run_remote_http_job(
   2182             vec![Ok(accepted)],
   2183             Vec::new(),
   2184             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2185         )
   2186         .await
   2187         .expect_err("cross-origin status url");
   2188 
   2189         assert!(matches!(
   2190             error,
   2191             TradeValidationReceiptJobError::RemoteHttpInvalidResponse("status_url")
   2192         ));
   2193         assert!(
   2194             trade_validation_receipt_test_hooks()
   2195                 .lock()
   2196                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2197                 .published_events
   2198                 .is_empty()
   2199         );
   2200     }
   2201 
   2202     #[cfg(feature = "sp1_verify")]
   2203     #[tokio::test]
   2204     async fn remote_http_prove_rejects_absolute_or_scheme_relative_status_path() {
   2205         let mut absolute = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2206         absolute.status_path = Some("https://example.invalid/status".to_string());
   2207         let error = run_remote_http_job(
   2208             vec![Ok(absolute)],
   2209             Vec::new(),
   2210             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2211         )
   2212         .await
   2213         .expect_err("absolute status path");
   2214 
   2215         assert!(matches!(
   2216             error,
   2217             TradeValidationReceiptJobError::RemoteHttpInvalidResponse("status_path")
   2218         ));
   2219 
   2220         let mut scheme_relative = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2221         scheme_relative.status_path = Some("//example.invalid/status".to_string());
   2222         let error = run_remote_http_job(
   2223             vec![Ok(scheme_relative)],
   2224             Vec::new(),
   2225             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2226         )
   2227         .await
   2228         .expect_err("scheme-relative status path");
   2229 
   2230         assert!(matches!(
   2231             error,
   2232             TradeValidationReceiptJobError::RemoteHttpInvalidResponse("status_path")
   2233         ));
   2234         assert!(
   2235             trade_validation_receipt_test_hooks()
   2236                 .lock()
   2237                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2238                 .published_events
   2239                 .is_empty()
   2240         );
   2241     }
   2242 
   2243     #[cfg(feature = "sp1_verify")]
   2244     #[tokio::test]
   2245     async fn remote_http_prove_rejects_polling_request_id_mismatch_before_next_poll() {
   2246         let mut accepted = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2247         accepted.request_id = "wrong-request".to_string();
   2248         accepted.status_path = Some("/prove/status/request-1".to_string());
   2249         let error = run_remote_http_job(
   2250             vec![
   2251                 Ok(accepted),
   2252                 Ok(remote_response(
   2253                     RadrootsSp1TradeRemoteProverStatus::Completed,
   2254                 )),
   2255             ],
   2256             Vec::new(),
   2257             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2258         )
   2259         .await
   2260         .expect_err("polling identity mismatch");
   2261 
   2262         assert!(matches!(
   2263             error,
   2264             TradeValidationReceiptJobError::RemoteHttpIdentityMismatch("request_id")
   2265         ));
   2266         assert!(
   2267             trade_validation_receipt_test_hooks()
   2268                 .lock()
   2269                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2270                 .published_events
   2271                 .is_empty()
   2272         );
   2273     }
   2274 
   2275     #[cfg(feature = "sp1_verify")]
   2276     #[tokio::test]
   2277     async fn remote_http_prove_does_not_publish_when_verification_fails() {
   2278         let error = run_remote_http_job(
   2279             vec![Ok(remote_response(
   2280                 RadrootsSp1TradeRemoteProverStatus::Completed,
   2281             ))],
   2282             vec![Err(TradeValidationReceiptJobError::Proof(
   2283                 RadrootsSp1TradeHostError::Sp1ProofVerificationFailed("test".to_string()),
   2284             ))],
   2285             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2286         )
   2287         .await
   2288         .expect_err("remote verification failure");
   2289 
   2290         assert!(matches!(
   2291             error,
   2292             TradeValidationReceiptJobError::Proof(
   2293                 RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(_)
   2294             )
   2295         ));
   2296         assert!(
   2297             trade_validation_receipt_test_hooks()
   2298                 .lock()
   2299                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2300                 .published_events
   2301                 .is_empty()
   2302         );
   2303     }
   2304 
   2305     #[cfg(feature = "sp1_verify")]
   2306     #[tokio::test]
   2307     async fn remote_http_prove_does_not_publish_when_reference_digest_mismatches() {
   2308         let mut response = remote_response(RadrootsSp1TradeRemoteProverStatus::Completed);
   2309         response.resolved_proof_envelope_base64 = Some("cHJvb2Y=".to_string());
   2310         let error = run_remote_http_job(
   2311             vec![Ok(response)],
   2312             Vec::new(),
   2313             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2314         )
   2315         .await
   2316         .expect_err("remote proof reference mismatch");
   2317 
   2318         assert!(matches!(
   2319             error,
   2320             TradeValidationReceiptJobError::Proof(
   2321                 RadrootsSp1TradeHostError::Sp1ProofReferenceDigestMismatch
   2322             )
   2323         ));
   2324         assert!(
   2325             trade_validation_receipt_test_hooks()
   2326                 .lock()
   2327                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2328                 .published_events
   2329                 .is_empty()
   2330         );
   2331     }
   2332 
   2333     #[cfg(feature = "sp1_verify")]
   2334     #[tokio::test]
   2335     async fn remote_http_prove_does_not_publish_when_sp1_identity_mismatches() {
   2336         let mut response = remote_response(RadrootsSp1TradeRemoteProverStatus::Completed);
   2337         response.sp1_program_hash = Some(hash32('c'));
   2338         let error = run_remote_http_job(
   2339             vec![Ok(response)],
   2340             vec![Ok(())],
   2341             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2342         )
   2343         .await
   2344         .expect_err("remote sp1 identity mismatch");
   2345 
   2346         assert!(matches!(
   2347             error,
   2348             TradeValidationReceiptJobError::RemoteHttpIdentityMismatch("sp1_program_hash")
   2349         ));
   2350         assert!(
   2351             trade_validation_receipt_test_hooks()
   2352                 .lock()
   2353                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2354                 .published_events
   2355                 .is_empty()
   2356         );
   2357     }
   2358 
   2359     #[cfg(feature = "sp1_verify")]
   2360     #[tokio::test]
   2361     async fn remote_http_prove_does_not_publish_when_public_values_mismatch() {
   2362         let mut response = remote_response(RadrootsSp1TradeRemoteProverStatus::Completed);
   2363         response.public_values_hash = Some(hash32('d'));
   2364         let error = run_remote_http_job(
   2365             vec![Ok(response)],
   2366             vec![Ok(())],
   2367             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2368         )
   2369         .await
   2370         .expect_err("remote public values mismatch");
   2371 
   2372         assert!(matches!(
   2373             error,
   2374             TradeValidationReceiptJobError::RemoteHttpIdentityMismatch("public_values_hash")
   2375         ));
   2376         assert!(
   2377             trade_validation_receipt_test_hooks()
   2378                 .lock()
   2379                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2380                 .published_events
   2381                 .is_empty()
   2382         );
   2383     }
   2384 
   2385     #[cfg(feature = "sp1_verify")]
   2386     #[tokio::test]
   2387     async fn remote_http_prove_does_not_publish_terminal_failed_or_rejected() {
   2388         let mut failed = remote_response(RadrootsSp1TradeRemoteProverStatus::Failed);
   2389         failed.reason_code = Some("remote_failed".to_string());
   2390         failed.message = Some("remote prover failed".to_string());
   2391         let error = run_remote_http_job(
   2392             vec![Ok(failed)],
   2393             Vec::new(),
   2394             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2395         )
   2396         .await
   2397         .expect_err("remote failed");
   2398         assert!(matches!(
   2399             error,
   2400             TradeValidationReceiptJobError::RemoteHttpTerminal {
   2401                 status: "failed",
   2402                 ..
   2403             }
   2404         ));
   2405         assert!(
   2406             trade_validation_receipt_test_hooks()
   2407                 .lock()
   2408                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2409                 .published_events
   2410                 .is_empty()
   2411         );
   2412 
   2413         let mut rejected = remote_response(RadrootsSp1TradeRemoteProverStatus::Rejected);
   2414         rejected.reason_code = Some("remote_rejected".to_string());
   2415         rejected.message = Some("remote prover rejected request".to_string());
   2416         let error = run_remote_http_job(
   2417             vec![Ok(rejected)],
   2418             Vec::new(),
   2419             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2420         )
   2421         .await
   2422         .expect_err("remote rejected");
   2423         assert!(matches!(
   2424             error,
   2425             TradeValidationReceiptJobError::RemoteHttpTerminal {
   2426                 status: "rejected",
   2427                 ..
   2428             }
   2429         ));
   2430         assert!(
   2431             trade_validation_receipt_test_hooks()
   2432                 .lock()
   2433                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2434                 .published_events
   2435                 .is_empty()
   2436         );
   2437     }
   2438 
   2439     #[cfg(feature = "sp1_verify")]
   2440     #[tokio::test]
   2441     async fn remote_http_prove_does_not_publish_timeout_or_oversized_response() {
   2442         let mut accepted = remote_response(RadrootsSp1TradeRemoteProverStatus::Accepted);
   2443         accepted.status_path = Some("/prove/status/request-1".to_string());
   2444         let mut running = remote_response(RadrootsSp1TradeRemoteProverStatus::Running);
   2445         running.status_path = Some("/prove/status/request-1".to_string());
   2446         let error = run_remote_http_job(
   2447             vec![Ok(accepted), Ok(running)],
   2448             Vec::new(),
   2449             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2450         )
   2451         .await
   2452         .expect_err("remote timeout");
   2453         assert!(matches!(
   2454             error,
   2455             TradeValidationReceiptJobError::RemoteHttpTimeout
   2456         ));
   2457         assert!(
   2458             trade_validation_receipt_test_hooks()
   2459                 .lock()
   2460                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2461                 .published_events
   2462                 .is_empty()
   2463         );
   2464 
   2465         let error = run_remote_http_job(
   2466             vec![Err(
   2467                 TradeValidationReceiptJobError::RemoteHttpResponseTooLarge,
   2468             )],
   2469             Vec::new(),
   2470             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2471         )
   2472         .await
   2473         .expect_err("remote oversized response");
   2474         assert!(matches!(
   2475             error,
   2476             TradeValidationReceiptJobError::RemoteHttpResponseTooLarge
   2477         ));
   2478         assert!(
   2479             trade_validation_receipt_test_hooks()
   2480                 .lock()
   2481                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2482                 .published_events
   2483                 .is_empty()
   2484         );
   2485     }
   2486 
   2487     #[cfg(feature = "sp1_verify")]
   2488     #[tokio::test]
   2489     async fn remote_http_prove_rejects_oversized_content_length_before_publish() {
   2490         let endpoint = remote_http_local_response_url(
   2491             "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}",
   2492         );
   2493         let mut policy = remote_http_policy();
   2494         {
   2495             let remote_http = policy.remote_http.as_mut().expect("remote config");
   2496             remote_http.endpoint_url = endpoint;
   2497             remote_http.max_response_bytes = 1;
   2498         }
   2499         let error = run_remote_http_job_with_policy(
   2500             policy,
   2501             Vec::new(),
   2502             Vec::new(),
   2503             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2504         )
   2505         .await
   2506         .expect_err("oversized content-length response");
   2507 
   2508         assert!(matches!(
   2509             error,
   2510             TradeValidationReceiptJobError::RemoteHttpResponseTooLarge
   2511         ));
   2512         assert!(
   2513             trade_validation_receipt_test_hooks()
   2514                 .lock()
   2515                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2516                 .published_events
   2517                 .is_empty()
   2518         );
   2519     }
   2520 
   2521     #[cfg(feature = "sp1_verify")]
   2522     #[tokio::test]
   2523     async fn remote_http_prove_rejects_oversized_http_response_before_publish() {
   2524         let endpoint = remote_http_local_response_url(
   2525             "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n2\r\n{}\r\n0\r\n\r\n",
   2526         );
   2527         let mut policy = remote_http_policy();
   2528         {
   2529             let remote_http = policy.remote_http.as_mut().expect("remote config");
   2530             remote_http.endpoint_url = endpoint;
   2531             remote_http.max_response_bytes = 1;
   2532         }
   2533         let error = run_remote_http_job_with_policy(
   2534             policy,
   2535             Vec::new(),
   2536             Vec::new(),
   2537             vec![Err(TradeValidationReceiptJobError::InvalidJobRequest)],
   2538         )
   2539         .await
   2540         .expect_err("oversized streamed response");
   2541 
   2542         assert!(matches!(
   2543             error,
   2544             TradeValidationReceiptJobError::RemoteHttpResponseTooLarge
   2545         ));
   2546         assert!(
   2547             trade_validation_receipt_test_hooks()
   2548                 .lock()
   2549                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2550                 .published_events
   2551                 .is_empty()
   2552         );
   2553     }
   2554 
   2555     #[tokio::test]
   2556     async fn proof_job_publishes_verified_receipt_and_result_after_proof_verification() {
   2557         let _guard = test_guard();
   2558         let worker = RadrootsNostrKeys::generate();
   2559         let requester = RadrootsNostrKeys::generate();
   2560         let buyer = RadrootsNostrKeys::generate();
   2561         let seller = RadrootsNostrKeys::generate();
   2562         let listing_event = listing_event(&seller);
   2563         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2564         let job = job_request(
   2565             &requester,
   2566             &worker,
   2567             &listing_event,
   2568             &request_event,
   2569             &decision_event,
   2570             RadrootsSp1TradeProofMode::None,
   2571             None,
   2572             None,
   2573         );
   2574 
   2575         {
   2576             let mut hooks = trade_validation_receipt_test_hooks()
   2577                 .lock()
   2578                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2579             hooks
   2580                 .fetch_event_by_id_results
   2581                 .push_back(Ok(listing_event.clone()));
   2582             hooks
   2583                 .fetch_event_by_id_results
   2584                 .push_back(Ok(request_event.clone()));
   2585             hooks
   2586                 .fetch_event_by_id_results
   2587                 .push_back(Ok(decision_event.clone()));
   2588             hooks
   2589                 .publish_event_results
   2590                 .push_back(Ok(publish_result_id(1)));
   2591             hooks
   2592                 .publish_event_results
   2593                 .push_back(Ok(publish_result_id(2)));
   2594         }
   2595 
   2596         handle_trade_validation_receipt_job_request(
   2597             &job,
   2598             &worker,
   2599             &client_for(&worker),
   2600             &deterministic_policy(),
   2601         )
   2602         .await
   2603         .expect("proof job");
   2604 
   2605         let published = trade_validation_receipt_test_hooks()
   2606             .lock()
   2607             .unwrap_or_else(std::sync::PoisonError::into_inner)
   2608             .published_events
   2609             .clone();
   2610         assert_eq!(published.len(), 2);
   2611         assert_eq!(published[0].kind, KIND_TRADE_VALIDATION_RECEIPT);
   2612         assert_eq!(published[1].kind, KIND_TRADE_TRANSITION_PROOF_RESULT);
   2613 
   2614         let receipt_event = radroots_events::RadrootsNostrEvent {
   2615             id: publish_result_id(1),
   2616             author: worker.public_key().to_string(),
   2617             created_at: 1,
   2618             kind: published[0].kind,
   2619             tags: published[0].tags.clone(),
   2620             content: published[0].content.clone(),
   2621             sig: super::zero_signature(),
   2622         };
   2623         let verified = verify_validation_receipt_event(
   2624             &receipt_event,
   2625             RadrootsValidationReceiptExpectedBinding {
   2626                 order_id: Some("order-1"),
   2627                 proof_system: Some(RadrootsValidationReceiptProofSystem::None),
   2628                 ..RadrootsValidationReceiptExpectedBinding::default()
   2629             },
   2630         )
   2631         .expect("receipt verifies");
   2632         let result: TradeValidationReceiptJobResult =
   2633             serde_json::from_str(&published[1].content).expect("result json");
   2634         assert_eq!(result.receipt_event_id, publish_result_id(1));
   2635         assert_eq!(
   2636             result.prover_backend,
   2637             TradeValidationReceiptProverBackend::DeterministicNone
   2638         );
   2639         assert!(!result.proof_generated);
   2640         assert_eq!(result.proof_mode, RadrootsSp1TradeProofMode::None);
   2641         assert!(!result.sp1_execute_checked);
   2642         assert!(result.sp1_execute_public_values_hash.is_none());
   2643         assert!(!result.cryptographic_proof_verified);
   2644         assert_eq!(
   2645             result.public_values_hash,
   2646             verified.receipt.public_values_hash
   2647         );
   2648         assert_eq!(result.worker_role.to_string(), "non_authoritative_prover");
   2649         assert!(published[1].tags.iter().any(|tag| {
   2650             tag.get(0).map(String::as_str) == Some("e")
   2651                 && tag.get(1).map(String::as_str) == Some(publish_result_id(1).as_str())
   2652                 && tag.get(4).map(String::as_str) == Some("receipt")
   2653         }));
   2654         assert!(published[1].tags.iter().any(|tag| {
   2655             tag.get(0).map(String::as_str) == Some("prover_backend")
   2656                 && tag.get(1).map(String::as_str) == Some("deterministic_none")
   2657         }));
   2658         assert!(published[1].tags.iter().any(|tag| {
   2659             tag.get(0).map(String::as_str) == Some("proof_mode")
   2660                 && tag.get(1).map(String::as_str) == Some("none")
   2661         }));
   2662     }
   2663 
   2664     #[tokio::test]
   2665     async fn proof_job_rejects_unverified_proof_before_publication() {
   2666         let _guard = test_guard();
   2667         let worker = RadrootsNostrKeys::generate();
   2668         let requester = RadrootsNostrKeys::generate();
   2669         let buyer = RadrootsNostrKeys::generate();
   2670         let seller = RadrootsNostrKeys::generate();
   2671         let listing_event = listing_event(&seller);
   2672         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2673         let job = job_request(
   2674             &requester,
   2675             &worker,
   2676             &listing_event,
   2677             &request_event,
   2678             &decision_event,
   2679             RadrootsSp1TradeProofMode::Compressed,
   2680             None,
   2681             None,
   2682         );
   2683 
   2684         {
   2685             let mut hooks = trade_validation_receipt_test_hooks()
   2686                 .lock()
   2687                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2688             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2689             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2690             hooks
   2691                 .fetch_event_by_id_results
   2692                 .push_back(Ok(decision_event));
   2693         }
   2694 
   2695         let error = handle_trade_validation_receipt_job_request(
   2696             &job,
   2697             &worker,
   2698             &client_for(&worker),
   2699             &deterministic_policy(),
   2700         )
   2701         .await
   2702         .expect_err("backend rejects sp1 proof claim");
   2703         assert!(matches!(
   2704             error,
   2705             TradeValidationReceiptJobError::ProverBackendPolicyMismatch
   2706         ));
   2707         assert_eq!(
   2708             trade_validation_receipt_test_hooks()
   2709                 .lock()
   2710                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2711                 .fetch_event_by_id_results
   2712                 .len(),
   2713             3
   2714         );
   2715         assert!(
   2716             trade_validation_receipt_test_hooks()
   2717                 .lock()
   2718                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2719                 .published_events
   2720                 .is_empty()
   2721         );
   2722     }
   2723 
   2724     #[tokio::test]
   2725     async fn proof_job_rejects_request_prover_backend_override_before_relay_fetch() {
   2726         let _guard = test_guard();
   2727         let worker = RadrootsNostrKeys::generate();
   2728         let requester = RadrootsNostrKeys::generate();
   2729         let buyer = RadrootsNostrKeys::generate();
   2730         let seller = RadrootsNostrKeys::generate();
   2731         let listing_event = listing_event(&seller);
   2732         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2733         let job = job_request(
   2734             &requester,
   2735             &worker,
   2736             &listing_event,
   2737             &request_event,
   2738             &decision_event,
   2739             RadrootsSp1TradeProofMode::None,
   2740             None,
   2741             None,
   2742         );
   2743         let mut request_json: serde_json::Value =
   2744             serde_json::from_str(&job.content).expect("request json");
   2745         request_json["prover_backend"] = serde_json::Value::String("local_cpu_prove".to_string());
   2746         let job = signed_event(
   2747             &requester,
   2748             KIND_TRADE_TRANSITION_PROOF_REQUEST,
   2749             serde_json::to_string(&request_json).expect("request json"),
   2750             vec![vec!["p".to_string(), worker.public_key().to_string()]],
   2751         );
   2752 
   2753         {
   2754             let mut hooks = trade_validation_receipt_test_hooks()
   2755                 .lock()
   2756                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2757             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2758             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2759             hooks
   2760                 .fetch_event_by_id_results
   2761                 .push_back(Ok(decision_event));
   2762         }
   2763 
   2764         let error = handle_trade_validation_receipt_job_request(
   2765             &job,
   2766             &worker,
   2767             &client_for(&worker),
   2768             &deterministic_policy(),
   2769         )
   2770         .await
   2771         .expect_err("request backend override rejected");
   2772         assert!(matches!(error, TradeValidationReceiptJobError::Serde(_)));
   2773         let hooks = trade_validation_receipt_test_hooks()
   2774             .lock()
   2775             .unwrap_or_else(std::sync::PoisonError::into_inner);
   2776         assert_eq!(hooks.fetch_event_by_id_results.len(), 3);
   2777         assert!(hooks.published_events.is_empty());
   2778     }
   2779 
   2780     #[tokio::test]
   2781     async fn proof_job_rejects_disabled_policy_before_relay_fetch() {
   2782         let _guard = test_guard();
   2783         let worker = RadrootsNostrKeys::generate();
   2784         let requester = RadrootsNostrKeys::generate();
   2785         let buyer = RadrootsNostrKeys::generate();
   2786         let seller = RadrootsNostrKeys::generate();
   2787         let listing_event = listing_event(&seller);
   2788         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2789         let job = job_request(
   2790             &requester,
   2791             &worker,
   2792             &listing_event,
   2793             &request_event,
   2794             &decision_event,
   2795             RadrootsSp1TradeProofMode::None,
   2796             None,
   2797             None,
   2798         );
   2799 
   2800         {
   2801             let mut hooks = trade_validation_receipt_test_hooks()
   2802                 .lock()
   2803                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2804             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2805             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2806             hooks
   2807                 .fetch_event_by_id_results
   2808                 .push_back(Ok(decision_event));
   2809         }
   2810 
   2811         let error = handle_trade_validation_receipt_job_request(
   2812             &job,
   2813             &worker,
   2814             &client_for(&worker),
   2815             &TradeValidationReceiptProverPolicy::default(),
   2816         )
   2817         .await
   2818         .expect_err("disabled policy rejected");
   2819         assert!(matches!(
   2820             error,
   2821             TradeValidationReceiptJobError::ProverBackendDisabled
   2822         ));
   2823         let hooks = trade_validation_receipt_test_hooks()
   2824             .lock()
   2825             .unwrap_or_else(std::sync::PoisonError::into_inner);
   2826         assert_eq!(hooks.fetch_event_by_id_results.len(), 3);
   2827         assert!(hooks.published_events.is_empty());
   2828     }
   2829 
   2830     #[tokio::test]
   2831     async fn proof_job_rejects_unverified_signed_event_evidence_before_publication() {
   2832         let _guard = test_guard();
   2833         let worker = RadrootsNostrKeys::generate();
   2834         let requester = RadrootsNostrKeys::generate();
   2835         let buyer = RadrootsNostrKeys::generate();
   2836         let seller = RadrootsNostrKeys::generate();
   2837         let listing_event = listing_event(&seller);
   2838         let (mut request_event, decision_event) =
   2839             signed_order_events(&buyer, &seller, &listing_event);
   2840         let job = job_request(
   2841             &requester,
   2842             &worker,
   2843             &listing_event,
   2844             &request_event,
   2845             &decision_event,
   2846             RadrootsSp1TradeProofMode::None,
   2847             None,
   2848             None,
   2849         );
   2850         request_event.content.push(' ');
   2851 
   2852         {
   2853             let mut hooks = trade_validation_receipt_test_hooks()
   2854                 .lock()
   2855                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2856             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2857             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2858             hooks
   2859                 .fetch_event_by_id_results
   2860                 .push_back(Ok(decision_event));
   2861         }
   2862 
   2863         let error = handle_trade_validation_receipt_job_request(
   2864             &job,
   2865             &worker,
   2866             &client_for(&worker),
   2867             &deterministic_policy(),
   2868         )
   2869         .await
   2870         .expect_err("signed evidence rejected");
   2871         assert!(matches!(
   2872             error,
   2873             TradeValidationReceiptJobError::InvalidSignedEvent
   2874         ));
   2875         assert!(
   2876             trade_validation_receipt_test_hooks()
   2877                 .lock()
   2878                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2879                 .published_events
   2880                 .is_empty()
   2881         );
   2882     }
   2883 
   2884     #[tokio::test]
   2885     async fn proof_job_rejects_identity_mismatch_before_relay_fetch() {
   2886         let _guard = test_guard();
   2887         let worker = RadrootsNostrKeys::generate();
   2888         let requester = RadrootsNostrKeys::generate();
   2889         let buyer = RadrootsNostrKeys::generate();
   2890         let seller = RadrootsNostrKeys::generate();
   2891         let listing_event = listing_event(&seller);
   2892         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2893         let job = job_request(
   2894             &requester,
   2895             &worker,
   2896             &listing_event,
   2897             &request_event,
   2898             &decision_event,
   2899             RadrootsSp1TradeProofMode::None,
   2900             None,
   2901             None,
   2902         );
   2903         let mut request: TradeValidationReceiptJobRequest =
   2904             serde_json::from_str(&job.content).expect("job request json");
   2905         request.reducer_program_hash =
   2906             "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   2907         let job = signed_event(
   2908             &requester,
   2909             KIND_TRADE_TRANSITION_PROOF_REQUEST,
   2910             serde_json::to_string(&request).expect("job json"),
   2911             vec![vec!["p".to_string(), worker.public_key().to_string()]],
   2912         );
   2913 
   2914         {
   2915             let mut hooks = trade_validation_receipt_test_hooks()
   2916                 .lock()
   2917                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2918             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2919             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2920             hooks
   2921                 .fetch_event_by_id_results
   2922                 .push_back(Ok(decision_event));
   2923         }
   2924 
   2925         let error = handle_trade_validation_receipt_job_request(
   2926             &job,
   2927             &worker,
   2928             &client_for(&worker),
   2929             &deterministic_policy(),
   2930         )
   2931         .await
   2932         .expect_err("identity mismatch rejected");
   2933         assert!(matches!(
   2934             error,
   2935             TradeValidationReceiptJobError::ExpectedReducerProgramHashMismatch
   2936         ));
   2937         let hooks = trade_validation_receipt_test_hooks()
   2938             .lock()
   2939             .unwrap_or_else(std::sync::PoisonError::into_inner);
   2940         assert_eq!(hooks.fetch_event_by_id_results.len(), 3);
   2941         assert!(hooks.published_events.is_empty());
   2942     }
   2943 
   2944     #[cfg(not(feature = "sp1_proving"))]
   2945     #[tokio::test]
   2946     async fn proof_job_rejects_unavailable_prover_backend_before_publication() {
   2947         let _guard = test_guard();
   2948         let worker = RadrootsNostrKeys::generate();
   2949         let requester = RadrootsNostrKeys::generate();
   2950         let buyer = RadrootsNostrKeys::generate();
   2951         let seller = RadrootsNostrKeys::generate();
   2952         let listing_event = listing_event(&seller);
   2953         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   2954         let job = job_request(
   2955             &requester,
   2956             &worker,
   2957             &listing_event,
   2958             &request_event,
   2959             &decision_event,
   2960             RadrootsSp1TradeProofMode::None,
   2961             None,
   2962             None,
   2963         );
   2964 
   2965         {
   2966             let mut hooks = trade_validation_receipt_test_hooks()
   2967                 .lock()
   2968                 .unwrap_or_else(std::sync::PoisonError::into_inner);
   2969             hooks.fetch_event_by_id_results.push_back(Ok(listing_event));
   2970             hooks.fetch_event_by_id_results.push_back(Ok(request_event));
   2971             hooks
   2972                 .fetch_event_by_id_results
   2973                 .push_back(Ok(decision_event));
   2974         }
   2975 
   2976         let local_execute_policy = TradeValidationReceiptProverPolicy {
   2977             backend: TradeValidationReceiptProverBackend::LocalExecute,
   2978             proof_mode: RadrootsSp1TradeProofMode::None,
   2979             expected_sp1_program_hash: None,
   2980             expected_sp1_verifying_key_hash: None,
   2981             remote_http: None,
   2982         };
   2983         let error = handle_trade_validation_receipt_job_request(
   2984             &job,
   2985             &worker,
   2986             &client_for(&worker),
   2987             &local_execute_policy,
   2988         )
   2989         .await
   2990         .expect_err("backend unavailable");
   2991         assert!(matches!(
   2992             error,
   2993             TradeValidationReceiptJobError::ProverBackendUnavailable("local_execute")
   2994         ));
   2995         assert_eq!(
   2996             trade_validation_receipt_test_hooks()
   2997                 .lock()
   2998                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   2999                 .fetch_event_by_id_results
   3000                 .len(),
   3001             3
   3002         );
   3003         assert!(
   3004             trade_validation_receipt_test_hooks()
   3005                 .lock()
   3006                 .unwrap_or_else(std::sync::PoisonError::into_inner)
   3007                 .published_events
   3008                 .is_empty()
   3009         );
   3010     }
   3011 
   3012     #[tokio::test]
   3013     async fn proof_job_requires_worker_recipient_tag() {
   3014         let _guard = test_guard();
   3015         let worker = RadrootsNostrKeys::generate();
   3016         let requester = RadrootsNostrKeys::generate();
   3017         let job = RadrootsNostrEventBuilder::new(
   3018             RadrootsNostrKind::Custom(KIND_TRADE_TRANSITION_PROOF_REQUEST as u16),
   3019             "{}",
   3020         )
   3021         .tags(vec![RadrootsNostrTag::custom(
   3022             RadrootsNostrTagKind::custom("p"),
   3023             vec![requester.public_key().to_string()],
   3024         )])
   3025         .sign_with_keys(&requester)
   3026         .expect("job");
   3027 
   3028         let error = handle_trade_validation_receipt_job_request(
   3029             &job,
   3030             &worker,
   3031             &client_for(&worker),
   3032             &deterministic_policy(),
   3033         )
   3034         .await
   3035         .expect_err("missing recipient");
   3036         assert!(matches!(
   3037             error,
   3038             TradeValidationReceiptJobError::MissingRecipient
   3039         ));
   3040     }
   3041 
   3042     trait WorkerRoleLabel {
   3043         fn to_string(self) -> String;
   3044     }
   3045 
   3046     impl WorkerRoleLabel for super::TradeValidationReceiptWorkerRole {
   3047         fn to_string(self) -> String {
   3048             serde_json::to_value(self)
   3049                 .expect("role json")
   3050                 .as_str()
   3051                 .expect("role string")
   3052                 .to_string()
   3053         }
   3054     }
   3055 
   3056     #[test]
   3057     fn signed_events_are_canonical_order_events() {
   3058         let _guard = test_guard();
   3059         let buyer = RadrootsNostrKeys::generate();
   3060         let seller = RadrootsNostrKeys::generate();
   3061         let listing_event = listing_event(&seller);
   3062         let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event);
   3063         let request_rr = radroots_event_from_nostr(&request_event);
   3064         let decision_rr = radroots_event_from_nostr(&decision_event);
   3065         assert!(
   3066             order_request_event_build(
   3067                 &RadrootsNostrEventPtr {
   3068                     id: listing_event.id.to_hex(),
   3069                     relays: None,
   3070                 },
   3071                 &request_payload(
   3072                     "order-1",
   3073                     &listing_addr_for_seller(&seller),
   3074                     &buyer,
   3075                     &seller
   3076                 ),
   3077             )
   3078             .is_ok()
   3079         );
   3080         assert_eq!(request_rr.kind, 3422);
   3081         assert_eq!(decision_rr.kind, 3423);
   3082     }
   3083 }