cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

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 }