validation_receipt.rs (66660B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 3 use radroots_events::kinds::{KIND_TRADE_TRANSITION_PROOF_RESULT, KIND_TRADE_VALIDATION_RECEIPT}; 4 use radroots_nostr::prelude::{ 5 RadrootsNostrEvent, RadrootsNostrEventId, RadrootsNostrFilter, RadrootsNostrKind, 6 radroots_event_from_nostr, radroots_nostr_filter_tag, 7 }; 8 use radroots_sp1_host_trade::verify_order_acceptance_validation_receipt_inline_sp1_proof; 9 use radroots_sp1_host_trade::{ 10 RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, RadrootsSp1TradeProverBackend, 11 RadrootsSp1TradeWorkerResultPayload, RadrootsSp1TradeWorkerResultStatus, 12 RadrootsSp1TradeWorkerRole, 13 }; 14 use radroots_trade::validation_receipt::{ 15 RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, 16 RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem, 17 RadrootsValidationReceiptResult, RadrootsValidationReceiptTags, RadrootsValidationReceiptType, 18 verify_validation_receipt_event, 19 }; 20 use serde::{Deserialize, Serialize}; 21 22 use crate::runtime::config::RuntimeConfig; 23 use crate::runtime::direct_relay::{ 24 DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, fetch_events_from_relays, 25 }; 26 use crate::view::runtime::{CommandDisposition, RelayFailureView}; 27 28 #[derive(Debug, Clone)] 29 pub struct ValidationReceiptEventArgs { 30 pub receipt_event_id: String, 31 } 32 33 #[derive(Debug, Clone)] 34 pub struct ValidationReceiptListArgs { 35 pub order_id: String, 36 } 37 38 #[derive(Debug, Clone, Serialize)] 39 pub struct ValidationReceiptInspectionView { 40 pub state: String, 41 pub resource: Option<ValidationReceiptResourceView>, 42 pub receipt_event_id: Option<String>, 43 pub order_id: Option<String>, 44 pub validation_state: String, 45 pub proof_verification: Option<ValidationReceiptProofVerificationView>, 46 pub receipt: Option<RadrootsTradeValidationReceipt>, 47 pub receipt_tags: Option<ValidationReceiptTagsView>, 48 pub event: Option<ValidationReceiptEventView>, 49 pub target_relays: Vec<String>, 50 pub connected_relays: Vec<String>, 51 pub failed_relays: Vec<RelayFailureView>, 52 pub reason_code: Option<String>, 53 pub reason: Option<String>, 54 pub actions: Vec<String>, 55 } 56 57 impl ValidationReceiptInspectionView { 58 pub fn disposition(&self) -> CommandDisposition { 59 match self.state.as_str() { 60 "valid" | "verified" => CommandDisposition::Success, 61 "missing" => CommandDisposition::NotFound, 62 "invalid" => CommandDisposition::ValidationFailed, 63 "unconfigured" => CommandDisposition::Unconfigured, 64 "network_unavailable" => CommandDisposition::ExternalUnavailable, 65 _ => CommandDisposition::InternalError, 66 } 67 } 68 } 69 70 #[derive(Debug, Clone, Serialize)] 71 pub struct ValidationReceiptListView { 72 pub state: String, 73 pub order_id: String, 74 pub count: usize, 75 pub valid_count: usize, 76 pub invalid_count: usize, 77 pub receipts: Vec<ValidationReceiptSummaryView>, 78 pub invalid_receipts: Vec<ValidationReceiptInvalidCandidateView>, 79 pub target_relays: Vec<String>, 80 pub connected_relays: Vec<String>, 81 pub failed_relays: Vec<RelayFailureView>, 82 pub reason_code: Option<String>, 83 pub reason: Option<String>, 84 pub actions: Vec<String>, 85 } 86 87 impl ValidationReceiptListView { 88 pub fn disposition(&self) -> CommandDisposition { 89 match self.state.as_str() { 90 "listed" | "empty" | "partial" => CommandDisposition::Success, 91 "invalid" => CommandDisposition::ValidationFailed, 92 "unconfigured" => CommandDisposition::Unconfigured, 93 "network_unavailable" => CommandDisposition::ExternalUnavailable, 94 _ => CommandDisposition::InternalError, 95 } 96 } 97 } 98 99 #[derive(Debug, Clone, Serialize)] 100 pub struct ValidationReceiptResourceView { 101 pub kind: String, 102 pub id: String, 103 } 104 105 #[derive(Debug, Clone, Serialize)] 106 pub struct ValidationReceiptEventView { 107 pub id: String, 108 pub author: String, 109 pub created_at: u32, 110 pub kind: u32, 111 pub sig: String, 112 pub tags: Vec<Vec<String>>, 113 pub content: String, 114 } 115 116 #[derive(Debug, Clone, Serialize)] 117 pub struct ValidationReceiptTagsView { 118 pub order_id: String, 119 pub event_set_root: String, 120 pub listing_event_id: String, 121 pub reducer_output_root: String, 122 pub public_values_hash: String, 123 pub proof_system: String, 124 pub receipt_type: String, 125 pub root_event_id: String, 126 pub target_event_id: String, 127 } 128 129 #[derive(Debug, Clone, Serialize)] 130 pub struct ValidationReceiptProofVerificationView { 131 pub state: String, 132 pub verifier: String, 133 pub proof_system: String, 134 pub public_values_hash_binding: String, 135 pub proof_metadata_binding: String, 136 pub cryptographic_proof_required: bool, 137 pub cryptographic_proof_verified: bool, 138 pub mode: Option<String>, 139 pub program_hash: Option<String>, 140 pub verifying_key_hash: Option<String>, 141 pub proof_reference: Option<String>, 142 pub inline_proof_present: bool, 143 pub worker_evidence: Option<ValidationReceiptWorkerEvidenceView>, 144 pub untrusted_worker_evidence: Option<ValidationReceiptWorkerEvidenceView>, 145 pub reason_code: Option<String>, 146 pub reason: Option<String>, 147 } 148 149 #[derive(Debug, Clone, Serialize)] 150 pub struct ValidationReceiptWorkerEvidenceView { 151 pub result_event_id: String, 152 pub author: String, 153 pub status: String, 154 pub prover_backend: String, 155 pub proof_mode: String, 156 pub proof_system: String, 157 pub proof_generated: bool, 158 pub sp1_execute_checked: bool, 159 pub sp1_execute_public_values_hash: Option<String>, 160 pub cryptographic_proof_verified: bool, 161 pub public_values_hash: String, 162 } 163 164 #[derive(Clone, Debug, Default)] 165 struct ValidationReceiptWorkerEvidenceSelection { 166 trusted: Option<ValidationReceiptWorkerEvidenceView>, 167 untrusted: Option<ValidationReceiptWorkerEvidenceView>, 168 } 169 170 #[derive(Debug, Clone, Serialize)] 171 pub struct ValidationReceiptSummaryView { 172 pub resource: ValidationReceiptResourceView, 173 pub receipt_event_id: String, 174 pub order_id: String, 175 pub author: String, 176 pub created_at: u32, 177 pub receipt_type: String, 178 pub result: String, 179 pub proof_system: String, 180 pub proof_verification_state: String, 181 pub event_set_root: String, 182 pub reducer_output_root: String, 183 pub public_values_hash: String, 184 } 185 186 #[derive(Debug, Clone, Serialize)] 187 pub struct ValidationReceiptInvalidCandidateView { 188 pub receipt_event_id: String, 189 pub kind: u32, 190 pub reason_code: String, 191 pub reason: String, 192 pub proof_verification: Option<ValidationReceiptProofVerificationView>, 193 } 194 195 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 196 enum ValidationReceiptCommandIntent { 197 Inspect, 198 Verify, 199 } 200 201 #[derive(Debug, Deserialize)] 202 struct RawValidationReceiptWorkerResultPayload { 203 cryptographic_proof_verified: bool, 204 decision_event_id: Option<String>, 205 event_set_root: Option<String>, 206 listing_event_id: Option<String>, 207 order_id: Option<String>, 208 proof_generated: bool, 209 proof_mode: String, 210 proof_system: String, 211 public_values_hash: String, 212 prover_backend: String, 213 receipt_kind: Option<u32>, 214 receipt_event_id: String, 215 reducer_output_root: Option<String>, 216 request_event_id: Option<String>, 217 sp1_execute_checked: bool, 218 sp1_execute_public_values_hash: Option<String>, 219 status: String, 220 worker_role: Option<String>, 221 } 222 223 impl RawValidationReceiptWorkerResultPayload { 224 fn typed(&self) -> Option<RadrootsSp1TradeWorkerResultPayload> { 225 Some(RadrootsSp1TradeWorkerResultPayload { 226 cryptographic_proof_verified: self.cryptographic_proof_verified, 227 decision_event_id: self.decision_event_id.clone(), 228 event_set_root: self.event_set_root.clone(), 229 listing_event_id: self.listing_event_id.clone(), 230 order_id: self.order_id.clone(), 231 proof_generated: self.proof_generated, 232 proof_mode: RadrootsSp1TradeProofMode::from_label(self.proof_mode.as_str())?, 233 proof_system: RadrootsValidationReceiptProofSystem::from_label( 234 self.proof_system.as_str(), 235 )?, 236 public_values_hash: self.public_values_hash.clone(), 237 prover_backend: RadrootsSp1TradeProverBackend::from_label( 238 self.prover_backend.as_str(), 239 )?, 240 receipt_event_id: self.receipt_event_id.clone(), 241 receipt_kind: self.receipt_kind, 242 reducer_output_root: self.reducer_output_root.clone(), 243 request_event_id: self.request_event_id.clone(), 244 sp1_execute_checked: self.sp1_execute_checked, 245 sp1_execute_public_values_hash: self.sp1_execute_public_values_hash.clone(), 246 status: match self.status.as_str() { 247 "succeeded" => RadrootsSp1TradeWorkerResultStatus::Succeeded, 248 _ => return None, 249 }, 250 worker_role: match self.worker_role.as_deref() { 251 Some("non_authoritative_prover") => { 252 Some(RadrootsSp1TradeWorkerRole::NonAuthoritativeProver) 253 } 254 Some(_) => return None, 255 None => None, 256 }, 257 }) 258 } 259 } 260 261 pub fn get( 262 config: &RuntimeConfig, 263 args: &ValidationReceiptEventArgs, 264 ) -> ValidationReceiptInspectionView { 265 inspect_event( 266 config, 267 &args.receipt_event_id, 268 "valid", 269 ValidationReceiptCommandIntent::Inspect, 270 ) 271 } 272 273 pub fn verify( 274 config: &RuntimeConfig, 275 args: &ValidationReceiptEventArgs, 276 ) -> ValidationReceiptInspectionView { 277 inspect_event( 278 config, 279 &args.receipt_event_id, 280 "verified", 281 ValidationReceiptCommandIntent::Verify, 282 ) 283 } 284 285 pub fn list(config: &RuntimeConfig, args: &ValidationReceiptListArgs) -> ValidationReceiptListView { 286 let order_id = args.order_id.trim(); 287 if order_id.is_empty() { 288 return invalid_list_view( 289 args.order_id.clone(), 290 "invalid_order_id", 291 "validation receipt list requires non-empty `order_id`", 292 ); 293 } 294 let filter = match validation_receipt_order_filter(order_id) { 295 Ok(filter) => filter, 296 Err(reason) => return invalid_list_view(order_id.to_owned(), "invalid_order_id", reason), 297 }; 298 let receipt = match fetch_events_from_relays(&config.relay.urls, filter) { 299 Ok(receipt) => receipt, 300 Err(error) => return list_fetch_error_view(order_id, error), 301 }; 302 list_from_fetch_receipt(config, order_id, receipt) 303 } 304 305 fn inspect_event( 306 config: &RuntimeConfig, 307 receipt_event_id: &str, 308 success_state: &str, 309 intent: ValidationReceiptCommandIntent, 310 ) -> ValidationReceiptInspectionView { 311 let receipt_event_id = receipt_event_id.trim(); 312 if receipt_event_id.is_empty() { 313 return invalid_inspection_view( 314 None, 315 "invalid_receipt_event_id", 316 "validation receipt command requires non-empty `receipt_event_id`", 317 ); 318 } 319 let event_id = match RadrootsNostrEventId::parse(receipt_event_id) { 320 Ok(event_id) => event_id, 321 Err(error) => { 322 return invalid_inspection_view( 323 Some(receipt_event_id.to_owned()), 324 "invalid_receipt_event_id", 325 format!("invalid validation receipt event id `{receipt_event_id}`: {error}"), 326 ); 327 } 328 }; 329 let filter = RadrootsNostrFilter::new().id(event_id); 330 let receipt = match fetch_events_from_relays(&config.relay.urls, filter) { 331 Ok(receipt) => receipt, 332 Err(error) => return inspection_fetch_error_view(receipt_event_id, error), 333 }; 334 inspection_from_fetch_receipt(config, receipt_event_id, success_state, intent, receipt) 335 } 336 337 fn validation_receipt_order_filter(order_id: &str) -> Result<RadrootsNostrFilter, String> { 338 let filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom( 339 KIND_TRADE_VALIDATION_RECEIPT as u16, 340 )); 341 radroots_nostr_filter_tag(filter, "d", vec![order_id.to_owned()]) 342 .map_err(|error| format!("build validation receipt order filter: {error}")) 343 } 344 345 fn inspection_from_fetch_receipt( 346 config: &RuntimeConfig, 347 receipt_event_id: &str, 348 success_state: &str, 349 intent: ValidationReceiptCommandIntent, 350 fetch_receipt: DirectRelayFetchReceipt, 351 ) -> ValidationReceiptInspectionView { 352 let DirectRelayFetchReceipt { 353 target_relays, 354 connected_relays, 355 failed_relays, 356 mut events, 357 } = fetch_receipt; 358 events.sort_by_key(|event| event.created_at.as_secs()); 359 let Some(event) = events.into_iter().next() else { 360 return ValidationReceiptInspectionView { 361 state: "missing".to_owned(), 362 resource: Some(validation_receipt_resource(receipt_event_id)), 363 receipt_event_id: Some(receipt_event_id.to_owned()), 364 order_id: None, 365 validation_state: "missing".to_owned(), 366 proof_verification: None, 367 receipt: None, 368 receipt_tags: None, 369 event: None, 370 target_relays, 371 connected_relays, 372 failed_relays: relay_failures(failed_relays), 373 reason_code: Some("validation_receipt_not_found".to_owned()), 374 reason: Some(format!( 375 "validation receipt event `{receipt_event_id}` was not found on configured relays" 376 )), 377 actions: Vec::new(), 378 }; 379 }; 380 inspected_event_view( 381 config, 382 success_state, 383 intent, 384 event, 385 target_relays, 386 connected_relays, 387 failed_relays, 388 ) 389 } 390 391 fn inspected_event_view( 392 config: &RuntimeConfig, 393 success_state: &str, 394 intent: ValidationReceiptCommandIntent, 395 event: RadrootsNostrEvent, 396 target_relays: Vec<String>, 397 connected_relays: Vec<String>, 398 failed_relays: Vec<DirectRelayFailure>, 399 ) -> ValidationReceiptInspectionView { 400 let converted = radroots_event_from_nostr(&event); 401 match verify_validation_receipt_event( 402 &converted, 403 RadrootsValidationReceiptExpectedBinding::default(), 404 ) { 405 Ok(verified) => { 406 let event_id = converted.id.clone(); 407 let order_id = verified.tags.order_id.clone(); 408 let proof_verification = 409 proof_verification_view(config, &event_id, &verified.receipt, &verified.tags); 410 let reason_code = 411 (!failed_relays.is_empty()).then_some("relay_fetch_partial".to_owned()); 412 let accepted = match intent { 413 ValidationReceiptCommandIntent::Inspect => { 414 !proof_state_is_invalid(proof_verification.state.as_str()) 415 } 416 ValidationReceiptCommandIntent::Verify => { 417 proof_state_is_verification_success(proof_verification.state.as_str()) 418 } 419 }; 420 if !accepted { 421 return ValidationReceiptInspectionView { 422 state: "invalid".to_owned(), 423 resource: Some(validation_receipt_resource(&event_id)), 424 receipt_event_id: Some(event_id), 425 order_id: Some(order_id), 426 validation_state: "invalid".to_owned(), 427 proof_verification: Some(proof_verification.clone()), 428 receipt: Some(verified.receipt), 429 receipt_tags: Some(tags_view(&verified.tags)), 430 event: Some(event_view(converted)), 431 target_relays, 432 connected_relays, 433 failed_relays: relay_failures(failed_relays), 434 reason_code: proof_verification.reason_code.clone(), 435 reason: proof_verification.reason.clone(), 436 actions: Vec::new(), 437 }; 438 } 439 ValidationReceiptInspectionView { 440 state: success_state.to_owned(), 441 resource: Some(validation_receipt_resource(&event_id)), 442 receipt_event_id: Some(event_id), 443 order_id: Some(order_id), 444 validation_state: "valid".to_owned(), 445 proof_verification: Some(proof_verification), 446 receipt: Some(verified.receipt), 447 receipt_tags: Some(tags_view(&verified.tags)), 448 event: Some(event_view(converted)), 449 target_relays, 450 connected_relays, 451 failed_relays: relay_failures(failed_relays), 452 reason_code, 453 reason: None, 454 actions: Vec::new(), 455 } 456 } 457 Err(error) => { 458 let reason_code = validation_receipt_invalid_reason_code(&error); 459 let proof_verification = invalid_proof_verification_view(&error); 460 ValidationReceiptInspectionView { 461 state: "invalid".to_owned(), 462 resource: Some(validation_receipt_resource(&converted.id)), 463 receipt_event_id: Some(converted.id.clone()), 464 order_id: None, 465 validation_state: "invalid".to_owned(), 466 proof_verification, 467 receipt: None, 468 receipt_tags: None, 469 event: Some(event_view(converted)), 470 target_relays, 471 connected_relays, 472 failed_relays: relay_failures(failed_relays), 473 reason_code: Some(reason_code.to_owned()), 474 reason: Some(error.to_string()), 475 actions: Vec::new(), 476 } 477 } 478 } 479 } 480 481 fn list_from_fetch_receipt( 482 config: &RuntimeConfig, 483 order_id: &str, 484 fetch_receipt: DirectRelayFetchReceipt, 485 ) -> ValidationReceiptListView { 486 let DirectRelayFetchReceipt { 487 target_relays, 488 connected_relays, 489 failed_relays, 490 mut events, 491 } = fetch_receipt; 492 events.sort_by(|left, right| { 493 left.created_at 494 .as_secs() 495 .cmp(&right.created_at.as_secs()) 496 .then_with(|| left.id.to_hex().cmp(&right.id.to_hex())) 497 }); 498 let mut verified_receipts = Vec::new(); 499 let mut invalid_receipts = Vec::new(); 500 501 for event in events { 502 let converted = radroots_event_from_nostr(&event); 503 match verify_validation_receipt_event( 504 &converted, 505 RadrootsValidationReceiptExpectedBinding { 506 order_id: Some(order_id), 507 ..RadrootsValidationReceiptExpectedBinding::default() 508 }, 509 ) { 510 Ok(verified) => { 511 verified_receipts.push((converted, verified.receipt, verified.tags)); 512 } 513 Err(error) => { 514 let reason_code = validation_receipt_invalid_reason_code(&error); 515 invalid_receipts.push(ValidationReceiptInvalidCandidateView { 516 receipt_event_id: converted.id, 517 kind: converted.kind, 518 reason_code: reason_code.to_owned(), 519 reason: error.to_string(), 520 proof_verification: invalid_proof_verification_view(&error), 521 }); 522 } 523 } 524 } 525 526 let evidence_bindings = verified_receipts 527 .iter() 528 .map(|(event, receipt, tags)| WorkerEvidenceReceiptBinding { 529 receipt_event_id: event.id.as_str(), 530 receipt, 531 tags, 532 }) 533 .collect::<Vec<_>>(); 534 let mut worker_evidence = worker_evidence_for_receipts(config, &evidence_bindings); 535 let mut receipts = Vec::new(); 536 for (event, receipt, tags) in verified_receipts { 537 let proof_verification = proof_verification_view_for_receipt( 538 &receipt, 539 worker_evidence 540 .remove(event.id.as_str()) 541 .unwrap_or_default(), 542 ); 543 if proof_state_is_invalid(proof_verification.state.as_str()) { 544 invalid_receipts.push(ValidationReceiptInvalidCandidateView { 545 receipt_event_id: event.id, 546 kind: event.kind, 547 reason_code: proof_verification 548 .reason_code 549 .clone() 550 .unwrap_or_else(|| proof_verification.state.clone()), 551 reason: proof_verification.reason.clone().unwrap_or_else(|| { 552 "validation receipt proof material did not verify".to_owned() 553 }), 554 proof_verification: Some(proof_verification), 555 }); 556 } else { 557 receipts.push(summary_view(&event, &receipt, &tags, &proof_verification)); 558 } 559 } 560 561 let failed_relays = relay_failures(failed_relays); 562 let valid_count = receipts.len(); 563 let invalid_count = invalid_receipts.len(); 564 let state = if valid_count > 0 && invalid_count > 0 { 565 "partial" 566 } else if valid_count > 0 { 567 "listed" 568 } else if invalid_count > 0 { 569 "invalid" 570 } else { 571 "empty" 572 }; 573 let reason_code = if invalid_count > 0 { 574 Some("validation_receipt_candidates_invalid".to_owned()) 575 } else if !failed_relays.is_empty() { 576 Some("relay_fetch_partial".to_owned()) 577 } else { 578 None 579 }; 580 let reason = match state { 581 "invalid" => Some(format!( 582 "found {invalid_count} invalid validation receipt candidate(s) and no valid receipts" 583 )), 584 "partial" => Some(format!( 585 "found {valid_count} valid receipt(s) and {invalid_count} invalid candidate(s)" 586 )), 587 _ => None, 588 }; 589 590 ValidationReceiptListView { 591 state: state.to_owned(), 592 order_id: order_id.to_owned(), 593 count: valid_count + invalid_count, 594 valid_count, 595 invalid_count, 596 receipts, 597 invalid_receipts, 598 target_relays, 599 connected_relays, 600 failed_relays, 601 reason_code, 602 reason, 603 actions: Vec::new(), 604 } 605 } 606 607 fn inspection_fetch_error_view( 608 receipt_event_id: &str, 609 error: DirectRelayFetchError, 610 ) -> ValidationReceiptInspectionView { 611 let (state, reason_code, reason, target_relays, connected_relays, failed_relays, actions) = 612 fetch_error_parts(error); 613 ValidationReceiptInspectionView { 614 state, 615 resource: Some(validation_receipt_resource(receipt_event_id)), 616 receipt_event_id: Some(receipt_event_id.to_owned()), 617 order_id: None, 618 validation_state: "unverified".to_owned(), 619 proof_verification: None, 620 receipt: None, 621 receipt_tags: None, 622 event: None, 623 target_relays, 624 connected_relays, 625 failed_relays, 626 reason_code: Some(reason_code), 627 reason: Some(reason), 628 actions, 629 } 630 } 631 632 fn list_fetch_error_view( 633 order_id: &str, 634 error: DirectRelayFetchError, 635 ) -> ValidationReceiptListView { 636 let (state, reason_code, reason, target_relays, connected_relays, failed_relays, actions) = 637 fetch_error_parts(error); 638 ValidationReceiptListView { 639 state, 640 order_id: order_id.to_owned(), 641 count: 0, 642 valid_count: 0, 643 invalid_count: 0, 644 receipts: Vec::new(), 645 invalid_receipts: Vec::new(), 646 target_relays, 647 connected_relays, 648 failed_relays, 649 reason_code: Some(reason_code), 650 reason: Some(reason), 651 actions, 652 } 653 } 654 655 fn fetch_error_parts( 656 error: DirectRelayFetchError, 657 ) -> ( 658 String, 659 String, 660 String, 661 Vec<String>, 662 Vec<String>, 663 Vec<RelayFailureView>, 664 Vec<String>, 665 ) { 666 match error { 667 DirectRelayFetchError::MissingRelays => ( 668 "unconfigured".to_owned(), 669 "relay_unconfigured".to_owned(), 670 "validation receipt commands require at least one configured relay".to_owned(), 671 Vec::new(), 672 Vec::new(), 673 Vec::new(), 674 vec![ 675 "radroots --relay wss://relay.example.com validation receipt list --order-id <order-id>" 676 .to_owned(), 677 ], 678 ), 679 DirectRelayFetchError::Connect { 680 reason, 681 target_relays, 682 failed_relays, 683 } => ( 684 "network_unavailable".to_owned(), 685 "relay_fetch_failed".to_owned(), 686 reason, 687 target_relays, 688 Vec::new(), 689 relay_failures(failed_relays), 690 Vec::new(), 691 ), 692 DirectRelayFetchError::RelayConfig { relay, source } => ( 693 "network_unavailable".to_owned(), 694 "relay_config_failed".to_owned(), 695 format!("failed to configure relay `{relay}` for validation receipt fetch: {source}"), 696 vec![relay.clone()], 697 Vec::new(), 698 vec![RelayFailureView { 699 relay, 700 reason: source.to_string(), 701 }], 702 Vec::new(), 703 ), 704 DirectRelayFetchError::Fetch(source) => ( 705 "network_unavailable".to_owned(), 706 "relay_fetch_failed".to_owned(), 707 source.to_string(), 708 Vec::new(), 709 Vec::new(), 710 Vec::new(), 711 Vec::new(), 712 ), 713 DirectRelayFetchError::Runtime(reason) => ( 714 "network_unavailable".to_owned(), 715 "relay_fetch_runtime_failed".to_owned(), 716 reason, 717 Vec::new(), 718 Vec::new(), 719 Vec::new(), 720 Vec::new(), 721 ), 722 } 723 } 724 725 fn invalid_inspection_view( 726 receipt_event_id: Option<String>, 727 reason_code: &str, 728 reason: impl Into<String>, 729 ) -> ValidationReceiptInspectionView { 730 ValidationReceiptInspectionView { 731 state: "invalid".to_owned(), 732 resource: receipt_event_id.as_deref().map(validation_receipt_resource), 733 receipt_event_id, 734 order_id: None, 735 validation_state: "invalid".to_owned(), 736 proof_verification: None, 737 receipt: None, 738 receipt_tags: None, 739 event: None, 740 target_relays: Vec::new(), 741 connected_relays: Vec::new(), 742 failed_relays: Vec::new(), 743 reason_code: Some(reason_code.to_owned()), 744 reason: Some(reason.into()), 745 actions: Vec::new(), 746 } 747 } 748 749 fn invalid_list_view( 750 order_id: String, 751 reason_code: &str, 752 reason: impl Into<String>, 753 ) -> ValidationReceiptListView { 754 ValidationReceiptListView { 755 state: "invalid".to_owned(), 756 order_id, 757 count: 0, 758 valid_count: 0, 759 invalid_count: 0, 760 receipts: Vec::new(), 761 invalid_receipts: Vec::new(), 762 target_relays: Vec::new(), 763 connected_relays: Vec::new(), 764 failed_relays: Vec::new(), 765 reason_code: Some(reason_code.to_owned()), 766 reason: Some(reason.into()), 767 actions: Vec::new(), 768 } 769 } 770 771 fn validation_receipt_resource(id: &str) -> ValidationReceiptResourceView { 772 ValidationReceiptResourceView { 773 kind: "validation_receipt".to_owned(), 774 id: id.to_owned(), 775 } 776 } 777 778 fn event_view(event: radroots_events::RadrootsNostrEvent) -> ValidationReceiptEventView { 779 ValidationReceiptEventView { 780 id: event.id, 781 author: event.author, 782 created_at: event.created_at, 783 kind: event.kind, 784 sig: event.sig, 785 tags: event.tags, 786 content: event.content, 787 } 788 } 789 790 fn tags_view(tags: &RadrootsValidationReceiptTags) -> ValidationReceiptTagsView { 791 ValidationReceiptTagsView { 792 order_id: tags.order_id.clone(), 793 event_set_root: tags.event_set_root.clone(), 794 listing_event_id: tags.listing_event_id.clone(), 795 reducer_output_root: tags.reducer_output_root.clone(), 796 public_values_hash: tags.public_values_hash.clone(), 797 proof_system: tags.proof_system.as_str().to_owned(), 798 receipt_type: receipt_type_label(tags.receipt_type).to_owned(), 799 root_event_id: tags.root_event_id.clone(), 800 target_event_id: tags.target_event_id.clone(), 801 } 802 } 803 804 fn proof_verification_view( 805 config: &RuntimeConfig, 806 receipt_event_id: &str, 807 receipt: &RadrootsTradeValidationReceipt, 808 tags: &RadrootsValidationReceiptTags, 809 ) -> ValidationReceiptProofVerificationView { 810 let worker_evidence = worker_evidence_for_receipt(config, receipt_event_id, receipt, tags); 811 proof_verification_view_for_receipt(receipt, worker_evidence) 812 } 813 814 fn proof_verification_view_for_receipt( 815 receipt: &RadrootsTradeValidationReceipt, 816 worker_evidence: ValidationReceiptWorkerEvidenceSelection, 817 ) -> ValidationReceiptProofVerificationView { 818 let proof = &receipt.proof; 819 let cryptographic_proof_required = proof.system != RadrootsValidationReceiptProofSystem::None; 820 if proof.system == RadrootsValidationReceiptProofSystem::None { 821 let state = if worker_evidence 822 .trusted 823 .as_ref() 824 .is_some_and(|evidence| evidence.sp1_execute_checked) 825 { 826 "sp1_execute_checked" 827 } else { 828 "deterministic_receipt_verified" 829 }; 830 return ValidationReceiptProofVerificationView { 831 state: state.to_owned(), 832 verifier: "radroots_cli_validation_receipt_v1".to_owned(), 833 proof_system: proof.system.as_str().to_owned(), 834 public_values_hash_binding: "verified".to_owned(), 835 proof_metadata_binding: "not_required".to_owned(), 836 cryptographic_proof_required, 837 cryptographic_proof_verified: false, 838 mode: proof.mode.clone(), 839 program_hash: proof.program_hash.clone(), 840 verifying_key_hash: proof.verifying_key_hash.clone(), 841 proof_reference: proof.proof_reference.clone(), 842 inline_proof_present: proof.inline_proof_base64.is_some(), 843 worker_evidence: worker_evidence.trusted, 844 untrusted_worker_evidence: worker_evidence.untrusted, 845 reason_code: None, 846 reason: None, 847 }; 848 } 849 if proof.proof_reference.is_some() { 850 return sp1_unverified_proof_view( 851 receipt, 852 worker_evidence, 853 "sp1_reference_unresolved", 854 "unverified", 855 "reference_unresolved", 856 Some("sp1_reference_unresolved"), 857 Some("SP1 proof reference resolution is not implemented by this CLI"), 858 ); 859 } 860 if proof.inline_proof_base64.is_none() { 861 return sp1_unverified_proof_view( 862 receipt, 863 worker_evidence, 864 "sp1_proof_material_missing", 865 "unverified", 866 "missing_proof_material", 867 Some("sp1_proof_material_missing"), 868 Some("SP1 proof material is missing"), 869 ); 870 } 871 if proof.system != RadrootsValidationReceiptProofSystem::Sp1Core { 872 return sp1_unverified_proof_view( 873 receipt, 874 worker_evidence, 875 "sp1_metadata_consistent_unverified", 876 "unverified", 877 "metadata_consistent_unverified", 878 Some("sp1_inline_proof_verification_unsupported"), 879 Some("only inline sp1_core proof verification is active in this CLI"), 880 ); 881 } 882 883 match verify_inline_sp1_receipt(receipt) { 884 Ok(()) => ValidationReceiptProofVerificationView { 885 state: "sp1_inline_proof_verified".to_owned(), 886 verifier: "radroots_cli_validation_receipt_v1".to_owned(), 887 proof_system: proof.system.as_str().to_owned(), 888 public_values_hash_binding: "verified".to_owned(), 889 proof_metadata_binding: "verified".to_owned(), 890 cryptographic_proof_required, 891 cryptographic_proof_verified: true, 892 mode: proof.mode.clone(), 893 program_hash: proof.program_hash.clone(), 894 verifying_key_hash: proof.verifying_key_hash.clone(), 895 proof_reference: proof.proof_reference.clone(), 896 inline_proof_present: proof.inline_proof_base64.is_some(), 897 worker_evidence: worker_evidence.trusted, 898 untrusted_worker_evidence: worker_evidence.untrusted, 899 reason_code: None, 900 reason: None, 901 }, 902 Err(error) => { 903 let mapped = proof_state_from_sp1_error(&error); 904 let reason = error.to_string(); 905 sp1_unverified_proof_view( 906 receipt, 907 worker_evidence, 908 mapped.state, 909 mapped.public_values_hash_binding, 910 mapped.proof_metadata_binding, 911 Some(mapped.reason_code), 912 Some(reason.as_str()), 913 ) 914 } 915 } 916 } 917 918 fn sp1_unverified_proof_view( 919 receipt: &RadrootsTradeValidationReceipt, 920 worker_evidence: ValidationReceiptWorkerEvidenceSelection, 921 state: &str, 922 public_values_hash_binding: &str, 923 proof_metadata_binding: &str, 924 reason_code: Option<&str>, 925 reason: Option<&str>, 926 ) -> ValidationReceiptProofVerificationView { 927 let proof = &receipt.proof; 928 ValidationReceiptProofVerificationView { 929 state: state.to_owned(), 930 verifier: "radroots_cli_validation_receipt_v1".to_owned(), 931 proof_system: proof.system.as_str().to_owned(), 932 public_values_hash_binding: public_values_hash_binding.to_owned(), 933 proof_metadata_binding: proof_metadata_binding.to_owned(), 934 cryptographic_proof_required: proof.system != RadrootsValidationReceiptProofSystem::None, 935 cryptographic_proof_verified: false, 936 mode: proof.mode.clone(), 937 program_hash: proof.program_hash.clone(), 938 verifying_key_hash: proof.verifying_key_hash.clone(), 939 proof_reference: proof.proof_reference.clone(), 940 inline_proof_present: proof.inline_proof_base64.is_some(), 941 worker_evidence: worker_evidence.trusted, 942 untrusted_worker_evidence: worker_evidence.untrusted, 943 reason_code: reason_code.map(str::to_owned), 944 reason: reason.map(str::to_owned), 945 } 946 } 947 948 fn validation_receipt_invalid_reason_code(error: &RadrootsValidationReceiptError) -> &'static str { 949 use radroots_trade::validation_receipt::RadrootsValidationReceiptError; 950 951 match error { 952 RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") 953 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => { 954 "sp1_proof_material_missing" 955 } 956 RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => { 957 "sp1_proof_material_conflict" 958 } 959 RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") => { 960 "sp1_inline_proof_invalid" 961 } 962 RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference") => { 963 "sp1_proof_reference_invalid" 964 } 965 RadrootsValidationReceiptError::TagMismatch("public_values_hash") => { 966 "public_values_hash_mismatch" 967 } 968 RadrootsValidationReceiptError::ExpectedBindingMismatch("public_values_hash") => { 969 "public_values_hash_mismatch" 970 } 971 RadrootsValidationReceiptError::ExpectedBindingMismatch("program_hash") => { 972 "sp1_program_hash_mismatch" 973 } 974 RadrootsValidationReceiptError::ExpectedBindingMismatch("verifying_key_hash") => { 975 "sp1_verifying_key_hash_mismatch" 976 } 977 _ => "validation_receipt_invalid", 978 } 979 } 980 981 fn invalid_proof_verification_view( 982 error: &RadrootsValidationReceiptError, 983 ) -> Option<ValidationReceiptProofVerificationView> { 984 let reason_code = validation_receipt_invalid_reason_code(error); 985 let (state, public_values_hash_binding, proof_metadata_binding) = match error { 986 RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") 987 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => ( 988 "sp1_proof_material_missing", 989 "unverified", 990 "missing_proof_material", 991 ), 992 RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => ( 993 "sp1_proof_material_conflict", 994 "unverified", 995 "conflicting_proof_material", 996 ), 997 RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") 998 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference") 999 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.mode") 1000 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.program_hash") 1001 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.verifying_key_hash") 1002 | RadrootsValidationReceiptError::InvalidProofMetadata("proof.system") 1003 | RadrootsValidationReceiptError::TagMismatch("proof_system") 1004 | RadrootsValidationReceiptError::ExpectedBindingMismatch("proof_system") => { 1005 ("sp1_proof_invalid", "unverified", "invalid") 1006 } 1007 RadrootsValidationReceiptError::TagMismatch("public_values_hash") 1008 | RadrootsValidationReceiptError::ExpectedBindingMismatch("public_values_hash") => ( 1009 "sp1_public_values_mismatch", 1010 "mismatch", 1011 "metadata_consistent", 1012 ), 1013 RadrootsValidationReceiptError::ExpectedBindingMismatch("program_hash") => { 1014 ("sp1_program_hash_mismatch", "unverified", "mismatch") 1015 } 1016 RadrootsValidationReceiptError::ExpectedBindingMismatch("verifying_key_hash") => { 1017 ("sp1_verifying_key_hash_mismatch", "unverified", "mismatch") 1018 } 1019 _ => return None, 1020 }; 1021 1022 Some(ValidationReceiptProofVerificationView { 1023 state: state.to_owned(), 1024 verifier: "radroots_cli_validation_receipt_v1".to_owned(), 1025 proof_system: "unknown".to_owned(), 1026 public_values_hash_binding: public_values_hash_binding.to_owned(), 1027 proof_metadata_binding: proof_metadata_binding.to_owned(), 1028 cryptographic_proof_required: true, 1029 cryptographic_proof_verified: false, 1030 mode: None, 1031 program_hash: None, 1032 verifying_key_hash: None, 1033 proof_reference: None, 1034 inline_proof_present: false, 1035 worker_evidence: None, 1036 untrusted_worker_evidence: None, 1037 reason_code: Some(reason_code.to_owned()), 1038 reason: Some(error.to_string()), 1039 }) 1040 } 1041 1042 struct MappedSp1ProofError { 1043 state: &'static str, 1044 public_values_hash_binding: &'static str, 1045 proof_metadata_binding: &'static str, 1046 reason_code: &'static str, 1047 } 1048 1049 fn proof_state_from_sp1_error(error: &RadrootsSp1TradeHostError) -> MappedSp1ProofError { 1050 match error { 1051 RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved => MappedSp1ProofError { 1052 state: "sp1_reference_unresolved", 1053 public_values_hash_binding: "unverified", 1054 proof_metadata_binding: "reference_unresolved", 1055 reason_code: "sp1_reference_unresolved", 1056 }, 1057 RadrootsSp1TradeHostError::MissingProofMaterial => MappedSp1ProofError { 1058 state: "sp1_proof_material_missing", 1059 public_values_hash_binding: "unverified", 1060 proof_metadata_binding: "missing_proof_material", 1061 reason_code: "sp1_proof_material_missing", 1062 }, 1063 RadrootsSp1TradeHostError::ProofMaterialConflict => MappedSp1ProofError { 1064 state: "sp1_proof_material_conflict", 1065 public_values_hash_binding: "unverified", 1066 proof_metadata_binding: "conflicting_proof_material", 1067 reason_code: "sp1_proof_material_conflict", 1068 }, 1069 RadrootsSp1TradeHostError::PublicValuesHashMismatch 1070 | RadrootsSp1TradeHostError::Sp1PublicValuesMismatch 1071 | RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch(_) => MappedSp1ProofError { 1072 state: "sp1_public_values_mismatch", 1073 public_values_hash_binding: "mismatch", 1074 proof_metadata_binding: "verified", 1075 reason_code: "sp1_public_values_mismatch", 1076 }, 1077 RadrootsSp1TradeHostError::Sp1ProgramHashMismatch 1078 | RadrootsSp1TradeHostError::MissingSp1ProgramHash => MappedSp1ProofError { 1079 state: "sp1_program_hash_mismatch", 1080 public_values_hash_binding: "unverified", 1081 proof_metadata_binding: "mismatch", 1082 reason_code: "sp1_program_hash_mismatch", 1083 }, 1084 RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch 1085 | RadrootsSp1TradeHostError::MissingVerifyingKeyHash => MappedSp1ProofError { 1086 state: "sp1_verifying_key_hash_mismatch", 1087 public_values_hash_binding: "unverified", 1088 proof_metadata_binding: "mismatch", 1089 reason_code: "sp1_verifying_key_hash_mismatch", 1090 }, 1091 RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable => MappedSp1ProofError { 1092 state: "sp1_verifier_unavailable", 1093 public_values_hash_binding: "unverified", 1094 proof_metadata_binding: "verifier_unavailable", 1095 reason_code: "sp1_verifier_unavailable", 1096 }, 1097 RadrootsSp1TradeHostError::Sp1SetupFailed(_) => MappedSp1ProofError { 1098 state: "sp1_verifier_setup_failed", 1099 public_values_hash_binding: "unverified", 1100 proof_metadata_binding: "verifier_setup_failed", 1101 reason_code: "sp1_verifier_setup_failed", 1102 }, 1103 _ => MappedSp1ProofError { 1104 state: "sp1_proof_invalid", 1105 public_values_hash_binding: "unverified", 1106 proof_metadata_binding: "invalid", 1107 reason_code: "sp1_proof_invalid", 1108 }, 1109 } 1110 } 1111 1112 fn verify_inline_sp1_receipt( 1113 receipt: &RadrootsTradeValidationReceipt, 1114 ) -> Result<(), RadrootsSp1TradeHostError> { 1115 let runtime = tokio::runtime::Builder::new_multi_thread() 1116 .enable_all() 1117 .build() 1118 .map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?; 1119 runtime 1120 .block_on(verify_order_acceptance_validation_receipt_inline_sp1_proof( 1121 receipt, 1122 )) 1123 .map(|_| ()) 1124 } 1125 1126 fn proof_state_is_invalid(state: &str) -> bool { 1127 matches!( 1128 state, 1129 "sp1_proof_material_missing" 1130 | "sp1_proof_material_conflict" 1131 | "sp1_public_values_mismatch" 1132 | "sp1_program_hash_mismatch" 1133 | "sp1_verifying_key_hash_mismatch" 1134 | "sp1_proof_invalid" 1135 ) 1136 } 1137 1138 fn proof_state_is_verification_success(state: &str) -> bool { 1139 matches!( 1140 state, 1141 "deterministic_receipt_verified" | "sp1_execute_checked" | "sp1_inline_proof_verified" 1142 ) 1143 } 1144 1145 fn validation_receipt_worker_result_filter( 1146 receipt_event_ids: Vec<String>, 1147 ) -> Result<RadrootsNostrFilter, String> { 1148 let filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom( 1149 KIND_TRADE_TRANSITION_PROOF_RESULT as u16, 1150 )); 1151 radroots_nostr_filter_tag(filter, "e", receipt_event_ids) 1152 .map_err(|error| format!("build validation receipt worker result filter: {error}")) 1153 } 1154 1155 struct WorkerEvidenceReceiptBinding<'a> { 1156 receipt_event_id: &'a str, 1157 receipt: &'a RadrootsTradeValidationReceipt, 1158 tags: &'a RadrootsValidationReceiptTags, 1159 } 1160 1161 fn worker_evidence_for_receipt( 1162 config: &RuntimeConfig, 1163 receipt_event_id: &str, 1164 receipt: &RadrootsTradeValidationReceipt, 1165 tags: &RadrootsValidationReceiptTags, 1166 ) -> ValidationReceiptWorkerEvidenceSelection { 1167 let bindings = [WorkerEvidenceReceiptBinding { 1168 receipt_event_id, 1169 receipt, 1170 tags, 1171 }]; 1172 worker_evidence_for_receipts(config, &bindings) 1173 .remove(receipt_event_id) 1174 .unwrap_or_default() 1175 } 1176 1177 fn worker_evidence_for_receipts( 1178 config: &RuntimeConfig, 1179 bindings: &[WorkerEvidenceReceiptBinding<'_>], 1180 ) -> BTreeMap<String, ValidationReceiptWorkerEvidenceSelection> { 1181 if config.rhi.trusted_worker_pubkeys.is_empty() || bindings.is_empty() { 1182 return BTreeMap::new(); 1183 } 1184 let receipt_event_ids = bindings 1185 .iter() 1186 .map(|binding| binding.receipt_event_id.to_owned()) 1187 .collect::<Vec<_>>(); 1188 let filter = match validation_receipt_worker_result_filter(receipt_event_ids) { 1189 Ok(filter) => filter, 1190 Err(_) => return BTreeMap::new(), 1191 }; 1192 let fetch_receipt = match fetch_events_from_relays(&config.relay.urls, filter) { 1193 Ok(fetch_receipt) => fetch_receipt, 1194 Err(_) => return BTreeMap::new(), 1195 }; 1196 let binding_by_receipt_id = bindings 1197 .iter() 1198 .map(|binding| (binding.receipt_event_id, binding)) 1199 .collect::<BTreeMap<_, _>>(); 1200 let trusted_pubkeys = config 1201 .rhi 1202 .trusted_worker_pubkeys 1203 .iter() 1204 .map(|pubkey| pubkey.to_ascii_lowercase()) 1205 .collect::<BTreeSet<_>>(); 1206 let mut by_receipt = 1207 BTreeMap::<String, Vec<(u64, String, bool, ValidationReceiptWorkerEvidenceView)>>::new(); 1208 1209 for event in fetch_receipt.events { 1210 let payload = 1211 match serde_json::from_str::<RawValidationReceiptWorkerResultPayload>(&event.content) { 1212 Ok(payload) => payload, 1213 Err(_) => continue, 1214 }; 1215 let Some(binding) = binding_by_receipt_id.get(payload.receipt_event_id.as_str()) else { 1216 continue; 1217 }; 1218 let converted = radroots_event_from_nostr(&event); 1219 let author = converted.author.to_ascii_lowercase(); 1220 let trusted_author = trusted_pubkeys.contains(author.as_str()); 1221 let typed_payload = payload.typed(); 1222 let bound = typed_payload 1223 .as_ref() 1224 .is_some_and(|payload| worker_payload_binds_receipt(payload, binding)); 1225 let trusted = trusted_author && bound; 1226 let receipt_event_id = payload.receipt_event_id.clone(); 1227 let result_event_id = event.id.to_hex(); 1228 let view = ValidationReceiptWorkerEvidenceView { 1229 result_event_id: result_event_id.clone(), 1230 author, 1231 status: payload.status, 1232 prover_backend: payload.prover_backend, 1233 proof_mode: payload.proof_mode, 1234 proof_system: payload.proof_system, 1235 proof_generated: payload.proof_generated, 1236 sp1_execute_checked: payload.sp1_execute_checked, 1237 sp1_execute_public_values_hash: payload.sp1_execute_public_values_hash, 1238 cryptographic_proof_verified: payload.cryptographic_proof_verified, 1239 public_values_hash: payload.public_values_hash, 1240 }; 1241 by_receipt.entry(receipt_event_id).or_default().push(( 1242 event.created_at.as_secs(), 1243 result_event_id, 1244 trusted, 1245 view, 1246 )); 1247 } 1248 1249 by_receipt 1250 .into_iter() 1251 .map(|(receipt_event_id, mut candidates)| { 1252 candidates.sort_by(|left, right| { 1253 left.0 1254 .cmp(&right.0) 1255 .then_with(|| left.1.cmp(&right.1)) 1256 .then_with(|| left.2.cmp(&right.2)) 1257 }); 1258 let mut selection = ValidationReceiptWorkerEvidenceSelection::default(); 1259 for (_, _, trusted, view) in candidates.into_iter().rev() { 1260 if trusted && selection.trusted.is_none() { 1261 selection.trusted = Some(view); 1262 } else if !trusted && selection.untrusted.is_none() { 1263 selection.untrusted = Some(view); 1264 } 1265 if selection.trusted.is_some() && selection.untrusted.is_some() { 1266 break; 1267 } 1268 } 1269 (receipt_event_id, selection) 1270 }) 1271 .collect() 1272 } 1273 1274 fn worker_payload_binds_receipt( 1275 payload: &RadrootsSp1TradeWorkerResultPayload, 1276 binding: &WorkerEvidenceReceiptBinding<'_>, 1277 ) -> bool { 1278 let receipt = binding.receipt; 1279 let tags = binding.tags; 1280 payload.status == RadrootsSp1TradeWorkerResultStatus::Succeeded 1281 && payload.worker_role == Some(RadrootsSp1TradeWorkerRole::NonAuthoritativeProver) 1282 && payload.receipt_kind == Some(KIND_TRADE_VALIDATION_RECEIPT) 1283 && payload.receipt_event_id == binding.receipt_event_id 1284 && payload.order_id.as_deref() == Some(tags.order_id.as_str()) 1285 && payload.listing_event_id.as_deref() == Some(tags.listing_event_id.as_str()) 1286 && payload.event_set_root.as_deref() == Some(tags.event_set_root.as_str()) 1287 && payload.reducer_output_root.as_deref() == Some(tags.reducer_output_root.as_str()) 1288 && payload.request_event_id.as_deref() == Some(tags.root_event_id.as_str()) 1289 && payload.decision_event_id.as_deref() == Some(tags.target_event_id.as_str()) 1290 && payload.public_values_hash == receipt.public_values_hash 1291 && payload.proof_system == receipt.proof.system 1292 && payload.proof_mode.mode_label().unwrap_or("none") 1293 == receipt.proof.mode.as_deref().unwrap_or("none") 1294 && payload.proof_generated 1295 == (receipt.proof.system != RadrootsValidationReceiptProofSystem::None) 1296 && payload.cryptographic_proof_verified == payload.proof_generated 1297 && payload.sp1_execute_checked 1298 && payload.sp1_execute_public_values_hash.as_deref() 1299 == Some(receipt.public_values_hash.as_str()) 1300 } 1301 1302 fn summary_view( 1303 event: &radroots_events::RadrootsNostrEvent, 1304 receipt: &RadrootsTradeValidationReceipt, 1305 tags: &RadrootsValidationReceiptTags, 1306 proof_verification: &ValidationReceiptProofVerificationView, 1307 ) -> ValidationReceiptSummaryView { 1308 ValidationReceiptSummaryView { 1309 resource: validation_receipt_resource(&event.id), 1310 receipt_event_id: event.id.clone(), 1311 order_id: tags.order_id.clone(), 1312 author: event.author.clone(), 1313 created_at: event.created_at, 1314 receipt_type: receipt_type_label(receipt.receipt_type).to_owned(), 1315 result: receipt_result_label(receipt.result).to_owned(), 1316 proof_system: receipt.proof.system.as_str().to_owned(), 1317 proof_verification_state: proof_verification.state.clone(), 1318 event_set_root: receipt.event_set_root.clone(), 1319 reducer_output_root: receipt.new_state_root.clone(), 1320 public_values_hash: receipt.public_values_hash.clone(), 1321 } 1322 } 1323 1324 fn receipt_type_label(value: RadrootsValidationReceiptType) -> &'static str { 1325 value.as_str() 1326 } 1327 1328 fn receipt_result_label(value: RadrootsValidationReceiptResult) -> &'static str { 1329 match value { 1330 RadrootsValidationReceiptResult::Valid => "valid", 1331 RadrootsValidationReceiptResult::Invalid => "invalid", 1332 } 1333 } 1334 1335 fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> { 1336 failures 1337 .into_iter() 1338 .map(|failure| RelayFailureView { 1339 relay: failure.relay, 1340 reason: failure.reason, 1341 }) 1342 .collect() 1343 } 1344 1345 #[cfg(test)] 1346 mod tests { 1347 use super::{ 1348 RawValidationReceiptWorkerResultPayload, ValidationReceiptWorkerEvidenceSelection, 1349 ValidationReceiptWorkerEvidenceView, WorkerEvidenceReceiptBinding, 1350 proof_state_from_sp1_error, proof_state_is_invalid, proof_verification_view_for_receipt, 1351 validation_receipt_invalid_reason_code, worker_payload_binds_receipt, 1352 }; 1353 use radroots_events::kinds::KIND_TRADE_VALIDATION_RECEIPT; 1354 use radroots_sp1_host_trade::RadrootsSp1TradeHostError; 1355 use radroots_trade::validation_receipt::{ 1356 RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, 1357 RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, 1358 RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, 1359 RadrootsValidationReceiptTags, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, 1360 VALIDATION_RECEIPT_VERSION, 1361 }; 1362 1363 fn sp1_proof_with_material() -> RadrootsValidationReceiptProof { 1364 RadrootsValidationReceiptProof { 1365 inline_proof_base64: Some("cHJvb2Y=".to_owned()), 1366 mode: Some("core".to_owned()), 1367 program_hash: Some( 1368 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(), 1369 ), 1370 proof_reference: None, 1371 system: RadrootsValidationReceiptProofSystem::Sp1Core, 1372 verifying_key_hash: Some( 1373 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(), 1374 ), 1375 } 1376 } 1377 1378 fn receipt_with_proof(proof: RadrootsValidationReceiptProof) -> RadrootsTradeValidationReceipt { 1379 RadrootsTradeValidationReceipt { 1380 changed_records_root: 1381 "0x1111111111111111111111111111111111111111111111111111111111111111".to_owned(), 1382 domain: VALIDATION_RECEIPT_DOMAIN.to_owned(), 1383 error_bitmap: "0x00000000000000000000000000000000".to_owned(), 1384 event_set_root: "0x2222222222222222222222222222222222222222222222222222222222222222" 1385 .to_owned(), 1386 new_state_root: "0x3333333333333333333333333333333333333333333333333333333333333333" 1387 .to_owned(), 1388 previous_state_root: 1389 "0x4444444444444444444444444444444444444444444444444444444444444444".to_owned(), 1390 proof, 1391 public_values_hash: 1392 "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), 1393 receipt_type: RadrootsValidationReceiptType::TradeTransition, 1394 result: RadrootsValidationReceiptResult::Valid, 1395 statement: RadrootsValidationReceiptStatement { 1396 listing_event_id: 1397 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(), 1398 root_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" 1399 .to_owned(), 1400 target_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" 1401 .to_owned(), 1402 statement_type: RadrootsValidationReceiptType::TradeTransition, 1403 }, 1404 version: VALIDATION_RECEIPT_VERSION, 1405 } 1406 } 1407 1408 fn deterministic_receipt() -> RadrootsTradeValidationReceipt { 1409 receipt_with_proof(RadrootsValidationReceiptProof { 1410 inline_proof_base64: None, 1411 mode: None, 1412 program_hash: None, 1413 proof_reference: None, 1414 system: RadrootsValidationReceiptProofSystem::None, 1415 verifying_key_hash: None, 1416 }) 1417 } 1418 1419 fn receipt_tags() -> RadrootsValidationReceiptTags { 1420 RadrootsValidationReceiptTags { 1421 event_set_root: "0x2222222222222222222222222222222222222222222222222222222222222222" 1422 .to_owned(), 1423 listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1424 .to_owned(), 1425 order_id: "order-1".to_owned(), 1426 proof_system: RadrootsValidationReceiptProofSystem::None, 1427 public_values_hash: 1428 "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), 1429 receipt_type: RadrootsValidationReceiptType::TradeTransition, 1430 reducer_output_root: 1431 "0x3333333333333333333333333333333333333333333333333333333333333333".to_owned(), 1432 root_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" 1433 .to_owned(), 1434 target_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" 1435 .to_owned(), 1436 } 1437 } 1438 1439 fn worker_result_payload(listing_event_id: &str) -> RawValidationReceiptWorkerResultPayload { 1440 RawValidationReceiptWorkerResultPayload { 1441 cryptographic_proof_verified: false, 1442 decision_event_id: Some( 1443 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_owned(), 1444 ), 1445 event_set_root: Some( 1446 "0x2222222222222222222222222222222222222222222222222222222222222222".to_owned(), 1447 ), 1448 listing_event_id: Some(listing_event_id.to_owned()), 1449 order_id: Some("order-1".to_owned()), 1450 proof_generated: false, 1451 proof_mode: "none".to_owned(), 1452 proof_system: "none".to_owned(), 1453 public_values_hash: 1454 "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), 1455 prover_backend: "local_execute".to_owned(), 1456 receipt_kind: Some(KIND_TRADE_VALIDATION_RECEIPT), 1457 receipt_event_id: "receipt-1".to_owned(), 1458 reducer_output_root: Some( 1459 "0x3333333333333333333333333333333333333333333333333333333333333333".to_owned(), 1460 ), 1461 request_event_id: Some( 1462 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(), 1463 ), 1464 sp1_execute_checked: true, 1465 sp1_execute_public_values_hash: Some( 1466 "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), 1467 ), 1468 status: "succeeded".to_owned(), 1469 worker_role: Some("non_authoritative_prover".to_owned()), 1470 } 1471 } 1472 1473 #[test] 1474 fn worker_evidence_binds_distinct_listing_request_and_decision_ids() { 1475 let receipt = deterministic_receipt(); 1476 let tags = receipt_tags(); 1477 let binding = WorkerEvidenceReceiptBinding { 1478 receipt_event_id: "receipt-1", 1479 receipt: &receipt, 1480 tags: &tags, 1481 }; 1482 1483 assert!(worker_payload_binds_receipt( 1484 &worker_result_payload( 1485 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1486 ) 1487 .typed() 1488 .expect("typed payload"), 1489 &binding 1490 )); 1491 } 1492 1493 #[test] 1494 fn worker_evidence_rejects_listing_id_mismatch() { 1495 let receipt = deterministic_receipt(); 1496 let tags = receipt_tags(); 1497 let binding = WorkerEvidenceReceiptBinding { 1498 receipt_event_id: "receipt-1", 1499 receipt: &receipt, 1500 tags: &tags, 1501 }; 1502 1503 assert!(!worker_payload_binds_receipt( 1504 &worker_result_payload( 1505 "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" 1506 ) 1507 .typed() 1508 .expect("typed payload"), 1509 &binding 1510 )); 1511 } 1512 1513 #[test] 1514 fn worker_evidence_unknown_typed_values_are_not_trusted() { 1515 let mut payload = worker_result_payload( 1516 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1517 ); 1518 payload.prover_backend = "future_backend".to_owned(); 1519 assert!(payload.typed().is_none()); 1520 } 1521 1522 #[test] 1523 fn none_receipts_report_deterministic_verification_without_crypto_claim() { 1524 let view = proof_verification_view_for_receipt( 1525 &deterministic_receipt(), 1526 ValidationReceiptWorkerEvidenceSelection::default(), 1527 ); 1528 1529 assert_eq!(view.state, "deterministic_receipt_verified"); 1530 assert!(!view.cryptographic_proof_required); 1531 assert!(!view.cryptographic_proof_verified); 1532 } 1533 1534 #[test] 1535 fn none_receipts_surface_advisory_sp1_execute_evidence() { 1536 let view = proof_verification_view_for_receipt( 1537 &deterministic_receipt(), 1538 ValidationReceiptWorkerEvidenceSelection { 1539 trusted: Some(ValidationReceiptWorkerEvidenceView { 1540 result_event_id: "result-1".to_owned(), 1541 author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1542 .to_owned(), 1543 status: "succeeded".to_owned(), 1544 prover_backend: "local_execute".to_owned(), 1545 proof_mode: "none".to_owned(), 1546 proof_system: "none".to_owned(), 1547 proof_generated: false, 1548 sp1_execute_checked: true, 1549 sp1_execute_public_values_hash: Some( 1550 "0x5555555555555555555555555555555555555555555555555555555555555555" 1551 .to_owned(), 1552 ), 1553 cryptographic_proof_verified: false, 1554 public_values_hash: 1555 "0x5555555555555555555555555555555555555555555555555555555555555555" 1556 .to_owned(), 1557 }), 1558 untrusted: None, 1559 }, 1560 ); 1561 1562 assert_eq!(view.state, "sp1_execute_checked"); 1563 assert!(!view.cryptographic_proof_required); 1564 assert!(!view.cryptographic_proof_verified); 1565 } 1566 1567 #[test] 1568 fn untrusted_worker_evidence_does_not_upgrade_deterministic_receipts() { 1569 let view = proof_verification_view_for_receipt( 1570 &deterministic_receipt(), 1571 ValidationReceiptWorkerEvidenceSelection { 1572 trusted: None, 1573 untrusted: Some(ValidationReceiptWorkerEvidenceView { 1574 result_event_id: "result-1".to_owned(), 1575 author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1576 .to_owned(), 1577 status: "succeeded".to_owned(), 1578 prover_backend: "local_execute".to_owned(), 1579 proof_mode: "none".to_owned(), 1580 proof_system: "none".to_owned(), 1581 proof_generated: false, 1582 sp1_execute_checked: true, 1583 sp1_execute_public_values_hash: Some( 1584 "0x5555555555555555555555555555555555555555555555555555555555555555" 1585 .to_owned(), 1586 ), 1587 cryptographic_proof_verified: false, 1588 public_values_hash: 1589 "0x5555555555555555555555555555555555555555555555555555555555555555" 1590 .to_owned(), 1591 }), 1592 }, 1593 ); 1594 1595 assert_eq!(view.state, "deterministic_receipt_verified"); 1596 assert!(view.worker_evidence.is_none()); 1597 assert!(view.untrusted_worker_evidence.is_some()); 1598 } 1599 1600 #[test] 1601 fn sp1_receipts_with_references_report_unresolved_without_crypto_claim() { 1602 let mut receipt = receipt_with_proof(sp1_proof_with_material()); 1603 receipt.proof.inline_proof_base64 = None; 1604 receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); 1605 1606 let view = proof_verification_view_for_receipt( 1607 &receipt, 1608 ValidationReceiptWorkerEvidenceSelection::default(), 1609 ); 1610 1611 assert_eq!(view.state, "sp1_reference_unresolved"); 1612 assert!(view.cryptographic_proof_required); 1613 assert!(!view.cryptographic_proof_verified); 1614 assert_eq!(view.proof_metadata_binding, "reference_unresolved"); 1615 } 1616 1617 #[cfg(feature = "sp1-verify")] 1618 #[test] 1619 fn invalid_inline_sp1_material_reports_invalid_proof_state() { 1620 let view = proof_verification_view_for_receipt( 1621 &receipt_with_proof(sp1_proof_with_material()), 1622 ValidationReceiptWorkerEvidenceSelection::default(), 1623 ); 1624 1625 assert_eq!(view.state, "sp1_proof_invalid"); 1626 assert!(view.cryptographic_proof_required); 1627 assert!(!view.cryptographic_proof_verified); 1628 assert_eq!(view.reason_code.as_deref(), Some("sp1_proof_invalid")); 1629 } 1630 1631 #[cfg(not(feature = "sp1-verify"))] 1632 #[test] 1633 fn inline_sp1_material_reports_unavailable_verifier_without_sp1_verify_feature() { 1634 let view = proof_verification_view_for_receipt( 1635 &receipt_with_proof(sp1_proof_with_material()), 1636 ValidationReceiptWorkerEvidenceSelection::default(), 1637 ); 1638 1639 assert_eq!(view.state, "sp1_verifier_unavailable"); 1640 assert!(view.cryptographic_proof_required); 1641 assert!(!view.cryptographic_proof_verified); 1642 assert_eq!(view.public_values_hash_binding, "unverified"); 1643 assert_eq!(view.proof_metadata_binding, "verifier_unavailable"); 1644 assert_eq!( 1645 view.reason_code.as_deref(), 1646 Some("sp1_verifier_unavailable") 1647 ); 1648 assert!(!proof_state_is_invalid(view.state.as_str())); 1649 } 1650 1651 #[test] 1652 fn sp1_setup_failed_reports_verifier_setup_failure_without_invalid_proof_state() { 1653 let mapped = proof_state_from_sp1_error(&RadrootsSp1TradeHostError::Sp1SetupFailed( 1654 "runtime unavailable".to_owned(), 1655 )); 1656 1657 assert_eq!(mapped.state, "sp1_verifier_setup_failed"); 1658 assert_eq!(mapped.public_values_hash_binding, "unverified"); 1659 assert_eq!(mapped.proof_metadata_binding, "verifier_setup_failed"); 1660 assert_eq!(mapped.reason_code, "sp1_verifier_setup_failed"); 1661 assert!(!proof_state_is_invalid(mapped.state)); 1662 } 1663 1664 #[test] 1665 fn invalid_receipt_errors_get_specific_reason_codes() { 1666 assert_eq!( 1667 validation_receipt_invalid_reason_code( 1668 &RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") 1669 ), 1670 "sp1_proof_material_missing" 1671 ); 1672 assert_eq!( 1673 validation_receipt_invalid_reason_code( 1674 &RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") 1675 ), 1676 "sp1_proof_material_missing" 1677 ); 1678 assert_eq!( 1679 validation_receipt_invalid_reason_code( 1680 &RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") 1681 ), 1682 "sp1_proof_material_conflict" 1683 ); 1684 assert_eq!( 1685 validation_receipt_invalid_reason_code(&RadrootsValidationReceiptError::TagMismatch( 1686 "public_values_hash" 1687 )), 1688 "public_values_hash_mismatch" 1689 ); 1690 assert_eq!( 1691 validation_receipt_invalid_reason_code( 1692 &RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") 1693 ), 1694 "sp1_inline_proof_invalid" 1695 ); 1696 assert_eq!( 1697 validation_receipt_invalid_reason_code( 1698 &RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference") 1699 ), 1700 "sp1_proof_reference_invalid" 1701 ); 1702 assert_eq!( 1703 validation_receipt_invalid_reason_code( 1704 &RadrootsValidationReceiptError::ExpectedBindingMismatch("program_hash") 1705 ), 1706 "sp1_program_hash_mismatch" 1707 ); 1708 assert_eq!( 1709 validation_receipt_invalid_reason_code( 1710 &RadrootsValidationReceiptError::ExpectedBindingMismatch("verifying_key_hash") 1711 ), 1712 "sp1_verifying_key_hash_mismatch" 1713 ); 1714 } 1715 }