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 }