cli

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

commit 0936bd5f84f50d053fa7f1e2e21a15374f53110b
parent 21f4f39e2bb2ab5b40b693fb93f524433f38cb18
Author: triesap <tyson@radroots.org>
Date:   Wed,  1 Jul 2026 08:53:11 +0000

cli: consume sdk validation receipt APIs

- route validation receipt list, get, and verify through the SDK trade facade
- remove direct validation receipt relay fetch and worker evidence selection
- preserve SDK error detail in validation receipt command output
- harden CLI source guards against validation receipt relay bypasses

Diffstat:
Msrc/runtime/sdk.rs | 31++++++++++++++++++++++++-------
Msrc/runtime/validation_receipt.rs | 1026++++++++++++++++++++++++++-----------------------------------------------------
2 files changed, 364 insertions(+), 693 deletions(-)

diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -884,13 +884,6 @@ mod tests { reason: "relay ingest into the derived projection cache", lifecycle: "retain until relay ingest and derived projection repair migrate to SDK APIs", }, - DirectRelayConsumerException { - path: "src/runtime/validation_receipt.rs", - required_tokens: &["fetch_events_from_relays", "DirectRelayFetchReceipt"], - owner: "validation.receipt.relay-reads", - reason: "non-migrated validation receipt relay inspection", - lifecycle: "retain until validation receipt inspection migrates to SDK APIs", - }, ]; const MIGRATED_CLI_PATH_GUARDS: &[MigratedCliPathGuard] = &[ @@ -970,6 +963,30 @@ mod tests { ], }, MigratedCliPathGuard { + label: "validation receipt SDK list", + path: "src/runtime/validation_receipt.rs", + start: "pub fn list(", + end: "fn inspect_event(", + required_tokens: &[ + "TradeValidationReceiptListRequest::parse", + ".validation_receipts()", + ".list(request)", + ], + }, + MigratedCliPathGuard { + label: "validation receipt SDK inspection", + path: "src/runtime/validation_receipt.rs", + start: "fn inspect_event(", + end: "fn inspection_from_sdk_receipt(", + required_tokens: &[ + "TradeValidationReceiptInspectRequest::parse", + "TradeValidationReceiptVerifyRequest::parse", + ".validation_receipts()", + ".inspect(request)", + ".verify(request)", + ], + }, + MigratedCliPathGuard { label: "trade submit", path: "src/runtime/order.rs", start: "fn propose_trade_via_sdk(", diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs @@ -1,28 +1,23 @@ -use std::collections::{BTreeMap, BTreeSet}; - -use radroots_events::kinds::{KIND_TRADE_TRANSITION_PROOF_RESULT, KIND_TRADE_VALIDATION_RECEIPT}; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, RadrootsNostrEventId, RadrootsNostrFilter, RadrootsNostrKind, - radroots_event_from_nostr, radroots_nostr_filter_tag, +use radroots_sdk::{ + RadrootsSdkError, TradeValidationReceiptEvent, TradeValidationReceiptInspectReceipt, + TradeValidationReceiptInspectRequest, TradeValidationReceiptInvalidCandidate, + TradeValidationReceiptListReceipt, TradeValidationReceiptListRequest, + TradeValidationReceiptRelayOutcomeKind, TradeValidationReceiptRelayOutcomeReceipt, + TradeValidationReceiptTags, TradeValidationReceiptVerifyRequest, + TradeValidationReceiptWorkerEvidence, + TradeValidationReceiptWorkerEvidenceSelection as SdkWorkerEvidenceSelection, }; +use radroots_sp1_host_trade::RadrootsSp1TradeHostError; use radroots_sp1_host_trade::verify_order_acceptance_validation_receipt_inline_sp1_proof; -use radroots_sp1_host_trade::{ - RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, RadrootsSp1TradeProverBackend, - RadrootsSp1TradeWorkerResultPayload, RadrootsSp1TradeWorkerResultStatus, - RadrootsSp1TradeWorkerRole, -}; use radroots_trade::validation_receipt::{ - RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, - RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem, - RadrootsValidationReceiptResult, RadrootsValidationReceiptTags, RadrootsValidationReceiptType, - verify_validation_receipt_event, + RadrootsTradeValidationReceipt, RadrootsValidationReceiptProofSystem, + RadrootsValidationReceiptResult, RadrootsValidationReceiptType, }; -use serde::{Deserialize, Serialize}; +use serde::Serialize; +use serde_json::Value; use crate::runtime::config::RuntimeConfig; -use crate::runtime::direct_relay::{ - DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, fetch_events_from_relays, -}; +use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession}; use crate::view::runtime::{CommandDisposition, RelayFailureView}; #[derive(Debug, Clone)] @@ -51,6 +46,7 @@ pub struct ValidationReceiptInspectionView { pub failed_relays: Vec<RelayFailureView>, pub reason_code: Option<String>, pub reason: Option<String>, + pub sdk_error: Option<Value>, pub actions: Vec<String>, } @@ -81,6 +77,7 @@ pub struct ValidationReceiptListView { pub failed_relays: Vec<RelayFailureView>, pub reason_code: Option<String>, pub reason: Option<String>, + pub sdk_error: Option<Value>, pub actions: Vec<String>, } @@ -198,66 +195,6 @@ enum ValidationReceiptCommandIntent { Verify, } -#[derive(Debug, Deserialize)] -struct RawValidationReceiptWorkerResultPayload { - cryptographic_proof_verified: bool, - decision_event_id: Option<String>, - event_set_root: Option<String>, - listing_event_id: Option<String>, - order_id: Option<String>, - proof_generated: bool, - proof_mode: String, - proof_system: String, - public_values_hash: String, - prover_backend: String, - receipt_kind: Option<u32>, - receipt_event_id: String, - reducer_output_root: Option<String>, - request_event_id: Option<String>, - sp1_execute_checked: bool, - sp1_execute_public_values_hash: Option<String>, - status: String, - worker_role: Option<String>, -} - -impl RawValidationReceiptWorkerResultPayload { - fn typed(&self) -> Option<RadrootsSp1TradeWorkerResultPayload> { - Some(RadrootsSp1TradeWorkerResultPayload { - cryptographic_proof_verified: self.cryptographic_proof_verified, - decision_event_id: self.decision_event_id.clone(), - event_set_root: self.event_set_root.clone(), - listing_event_id: self.listing_event_id.clone(), - order_id: self.order_id.clone(), - proof_generated: self.proof_generated, - proof_mode: RadrootsSp1TradeProofMode::from_label(self.proof_mode.as_str())?, - proof_system: RadrootsValidationReceiptProofSystem::from_label( - self.proof_system.as_str(), - )?, - public_values_hash: self.public_values_hash.clone(), - prover_backend: RadrootsSp1TradeProverBackend::from_label( - self.prover_backend.as_str(), - )?, - receipt_event_id: self.receipt_event_id.clone(), - receipt_kind: self.receipt_kind, - reducer_output_root: self.reducer_output_root.clone(), - request_event_id: self.request_event_id.clone(), - sp1_execute_checked: self.sp1_execute_checked, - sp1_execute_public_values_hash: self.sp1_execute_public_values_hash.clone(), - status: match self.status.as_str() { - "succeeded" => RadrootsSp1TradeWorkerResultStatus::Succeeded, - _ => return None, - }, - worker_role: match self.worker_role.as_deref() { - Some("non_authoritative_prover") => { - Some(RadrootsSp1TradeWorkerRole::NonAuthoritativeProver) - } - Some(_) => return None, - None => None, - }, - }) - } -} - pub fn get( config: &RuntimeConfig, args: &ValidationReceiptEventArgs, @@ -291,15 +228,24 @@ pub fn list(config: &RuntimeConfig, args: &ValidationReceiptListArgs) -> Validat "validation receipt list requires non-empty `order_id`", ); } - let filter = match validation_receipt_order_filter(order_id) { - Ok(filter) => filter, - Err(reason) => return invalid_list_view(order_id.to_owned(), "invalid_order_id", reason), + let request = match TradeValidationReceiptListRequest::parse(order_id).and_then(|request| { + request.try_with_trusted_worker_pubkeys( + config.rhi.trusted_worker_pubkeys.iter().map(String::as_str), + ) + }) { + Ok(request) => request, + Err(error) => { + return list_sdk_error_view(order_id, CliSdkAdapterError::Sdk(error)); + } }; - let receipt = match fetch_events_from_relays(&config.relay.urls, filter) { - Ok(receipt) => receipt, - Err(error) => return list_fetch_error_view(order_id, error), + let session = match CliSdkSession::connect(config) { + Ok(session) => session, + Err(error) => return list_sdk_error_view(order_id, error), }; - list_from_fetch_receipt(config, order_id, receipt) + match session.block_on(session.sdk().trades().validation_receipts().list(request)) { + Ok(receipt) => list_from_sdk_receipt(receipt), + Err(error) => list_sdk_error_view(order_id, CliSdkAdapterError::Sdk(error)), + } } fn inspect_event( @@ -316,47 +262,82 @@ fn inspect_event( "validation receipt command requires non-empty `receipt_event_id`", ); } - let event_id = match RadrootsNostrEventId::parse(receipt_event_id) { - Ok(event_id) => event_id, - Err(error) => { - return invalid_inspection_view( - Some(receipt_event_id.to_owned()), - "invalid_receipt_event_id", - format!("invalid validation receipt event id `{receipt_event_id}`: {error}"), - ); - } + let trusted_worker_pubkeys = config.rhi.trusted_worker_pubkeys.iter().map(String::as_str); + let session = match CliSdkSession::connect(config) { + Ok(session) => session, + Err(error) => return inspection_sdk_error_view(receipt_event_id, error), }; - let filter = RadrootsNostrFilter::new().id(event_id); - let receipt = match fetch_events_from_relays(&config.relay.urls, filter) { - Ok(receipt) => receipt, - Err(error) => return inspection_fetch_error_view(receipt_event_id, error), - }; - inspection_from_fetch_receipt(config, receipt_event_id, success_state, intent, receipt) -} - -fn validation_receipt_order_filter(order_id: &str) -> Result<RadrootsNostrFilter, String> { - let filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom( - KIND_TRADE_VALIDATION_RECEIPT as u16, - )); - radroots_nostr_filter_tag(filter, "d", vec![order_id.to_owned()]) - .map_err(|error| format!("build validation receipt order filter: {error}")) + match intent { + ValidationReceiptCommandIntent::Inspect => { + let request = match TradeValidationReceiptInspectRequest::parse(receipt_event_id) + .and_then(|request| request.try_with_trusted_worker_pubkeys(trusted_worker_pubkeys)) + { + Ok(request) => request, + Err(error) => { + return inspection_sdk_error_view( + receipt_event_id, + CliSdkAdapterError::Sdk(error), + ); + } + }; + match session.block_on( + session + .sdk() + .trades() + .validation_receipts() + .inspect(request), + ) { + Ok(receipt) => { + inspection_from_sdk_receipt(receipt_event_id, success_state, intent, receipt) + } + Err(error) => { + inspection_sdk_error_view(receipt_event_id, CliSdkAdapterError::Sdk(error)) + } + } + } + ValidationReceiptCommandIntent::Verify => { + let request = match TradeValidationReceiptVerifyRequest::parse(receipt_event_id) + .and_then(|request| request.try_with_trusted_worker_pubkeys(trusted_worker_pubkeys)) + { + Ok(request) => request, + Err(error) => { + return inspection_sdk_error_view( + receipt_event_id, + CliSdkAdapterError::Sdk(error), + ); + } + }; + match session.block_on(session.sdk().trades().validation_receipts().verify(request)) { + Ok(receipt) => { + inspection_from_sdk_receipt(receipt_event_id, success_state, intent, receipt) + } + Err(error) => { + inspection_sdk_error_view(receipt_event_id, CliSdkAdapterError::Sdk(error)) + } + } + } + } } -fn inspection_from_fetch_receipt( - config: &RuntimeConfig, +fn inspection_from_sdk_receipt( receipt_event_id: &str, success_state: &str, intent: ValidationReceiptCommandIntent, - fetch_receipt: DirectRelayFetchReceipt, + sdk_receipt: TradeValidationReceiptInspectReceipt, ) -> ValidationReceiptInspectionView { - let DirectRelayFetchReceipt { - target_relays, - connected_relays, - failed_relays, - mut events, - } = fetch_receipt; - events.sort_by_key(|event| event.created_at.as_secs()); - let Some(event) = events.into_iter().next() else { + let target_relays = sdk_receipt.relay_targets; + let connected_relays = connected_relays(&sdk_receipt.relay_evidence.relays); + let failed_relays = sdk_relay_failures(&sdk_receipt.relay_evidence.relays); + let reason_code = (!failed_relays.is_empty()).then_some("relay_fetch_partial".to_owned()); + if let Some(invalid) = sdk_receipt.invalid_receipt { + return invalid_inspected_event_view( + invalid, + target_relays, + connected_relays, + failed_relays, + ); + } + let Some(receipt) = sdk_receipt.receipt else { return ValidationReceiptInspectionView { state: "missing".to_owned(), resource: Some(validation_receipt_resource(receipt_event_id)), @@ -369,181 +350,136 @@ fn inspection_from_fetch_receipt( event: None, target_relays, connected_relays, - failed_relays: relay_failures(failed_relays), + failed_relays, reason_code: Some("validation_receipt_not_found".to_owned()), reason: Some(format!( "validation receipt event `{receipt_event_id}` was not found on configured relays" )), + sdk_error: None, actions: Vec::new(), }; }; inspected_event_view( - config, + receipt, success_state, intent, - event, target_relays, connected_relays, failed_relays, + reason_code, ) } fn inspected_event_view( - config: &RuntimeConfig, + sdk_receipt: TradeValidationReceiptEvent, success_state: &str, intent: ValidationReceiptCommandIntent, - event: RadrootsNostrEvent, target_relays: Vec<String>, connected_relays: Vec<String>, - failed_relays: Vec<DirectRelayFailure>, + failed_relays: Vec<RelayFailureView>, + relay_reason_code: Option<String>, ) -> ValidationReceiptInspectionView { - let converted = radroots_event_from_nostr(&event); - match verify_validation_receipt_event( - &converted, - RadrootsValidationReceiptExpectedBinding::default(), - ) { - Ok(verified) => { - let event_id = converted.id.clone(); - let order_id = verified.tags.order_id.clone(); - let proof_verification = - proof_verification_view(config, &event_id, &verified.receipt, &verified.tags); - let reason_code = - (!failed_relays.is_empty()).then_some("relay_fetch_partial".to_owned()); - let accepted = match intent { - ValidationReceiptCommandIntent::Inspect => { - !proof_state_is_invalid(proof_verification.state.as_str()) - } - ValidationReceiptCommandIntent::Verify => { - proof_state_is_verification_success(proof_verification.state.as_str()) - } - }; - if !accepted { - return ValidationReceiptInspectionView { - state: "invalid".to_owned(), - resource: Some(validation_receipt_resource(&event_id)), - receipt_event_id: Some(event_id), - order_id: Some(order_id), - validation_state: "invalid".to_owned(), - proof_verification: Some(proof_verification.clone()), - receipt: Some(verified.receipt), - receipt_tags: Some(tags_view(&verified.tags)), - event: Some(event_view(converted)), - target_relays, - connected_relays, - failed_relays: relay_failures(failed_relays), - reason_code: proof_verification.reason_code.clone(), - reason: proof_verification.reason.clone(), - actions: Vec::new(), - }; - } - ValidationReceiptInspectionView { - state: success_state.to_owned(), - resource: Some(validation_receipt_resource(&event_id)), - receipt_event_id: Some(event_id), - order_id: Some(order_id), - validation_state: "valid".to_owned(), - proof_verification: Some(proof_verification), - receipt: Some(verified.receipt), - receipt_tags: Some(tags_view(&verified.tags)), - event: Some(event_view(converted)), - target_relays, - connected_relays, - failed_relays: relay_failures(failed_relays), - reason_code, - reason: None, - actions: Vec::new(), - } + let event_id = sdk_receipt.event.id.clone(); + let order_id = sdk_receipt.tags.order_id.clone(); + let proof_verification = proof_verification_view_for_receipt( + &sdk_receipt.receipt, + sdk_worker_evidence_selection(sdk_receipt.worker_evidence), + ); + let accepted = match intent { + ValidationReceiptCommandIntent::Inspect => { + !proof_state_is_invalid(proof_verification.state.as_str()) } - Err(error) => { - let reason_code = validation_receipt_invalid_reason_code(&error); - let proof_verification = invalid_proof_verification_view(&error); - ValidationReceiptInspectionView { - state: "invalid".to_owned(), - resource: Some(validation_receipt_resource(&converted.id)), - receipt_event_id: Some(converted.id.clone()), - order_id: None, - validation_state: "invalid".to_owned(), - proof_verification, - receipt: None, - receipt_tags: None, - event: Some(event_view(converted)), - target_relays, - connected_relays, - failed_relays: relay_failures(failed_relays), - reason_code: Some(reason_code.to_owned()), - reason: Some(error.to_string()), - actions: Vec::new(), - } + ValidationReceiptCommandIntent::Verify => { + proof_state_is_verification_success(proof_verification.state.as_str()) } + }; + if !accepted { + return ValidationReceiptInspectionView { + state: "invalid".to_owned(), + resource: Some(validation_receipt_resource(&event_id)), + receipt_event_id: Some(event_id), + order_id: Some(order_id), + validation_state: "invalid".to_owned(), + proof_verification: Some(proof_verification.clone()), + receipt: Some(sdk_receipt.receipt), + receipt_tags: Some(tags_view(&sdk_receipt.tags)), + event: Some(event_view(sdk_receipt.event)), + target_relays, + connected_relays, + failed_relays, + reason_code: proof_verification.reason_code.clone(), + reason: proof_verification.reason.clone(), + sdk_error: None, + actions: Vec::new(), + }; + } + ValidationReceiptInspectionView { + state: success_state.to_owned(), + resource: Some(validation_receipt_resource(&event_id)), + receipt_event_id: Some(event_id), + order_id: Some(order_id), + validation_state: "valid".to_owned(), + proof_verification: Some(proof_verification), + receipt: Some(sdk_receipt.receipt), + receipt_tags: Some(tags_view(&sdk_receipt.tags)), + event: Some(event_view(sdk_receipt.event)), + target_relays, + connected_relays, + failed_relays, + reason_code: relay_reason_code, + reason: None, + sdk_error: None, + actions: Vec::new(), } } -fn list_from_fetch_receipt( - config: &RuntimeConfig, - order_id: &str, - fetch_receipt: DirectRelayFetchReceipt, -) -> ValidationReceiptListView { - let DirectRelayFetchReceipt { +fn invalid_inspected_event_view( + invalid: TradeValidationReceiptInvalidCandidate, + target_relays: Vec<String>, + connected_relays: Vec<String>, + failed_relays: Vec<RelayFailureView>, +) -> ValidationReceiptInspectionView { + ValidationReceiptInspectionView { + state: "invalid".to_owned(), + resource: Some(validation_receipt_resource(&invalid.event.id)), + receipt_event_id: Some(invalid.event.id.clone()), + order_id: None, + validation_state: "invalid".to_owned(), + proof_verification: None, + receipt: None, + receipt_tags: None, + event: Some(event_view(invalid.event)), target_relays, connected_relays, failed_relays, - mut events, - } = fetch_receipt; - events.sort_by(|left, right| { - left.created_at - .as_secs() - .cmp(&right.created_at.as_secs()) - .then_with(|| left.id.to_hex().cmp(&right.id.to_hex())) - }); - let mut verified_receipts = Vec::new(); - let mut invalid_receipts = Vec::new(); - - for event in events { - let converted = radroots_event_from_nostr(&event); - match verify_validation_receipt_event( - &converted, - RadrootsValidationReceiptExpectedBinding { - order_id: Some(order_id), - ..RadrootsValidationReceiptExpectedBinding::default() - }, - ) { - Ok(verified) => { - verified_receipts.push((converted, verified.receipt, verified.tags)); - } - Err(error) => { - let reason_code = validation_receipt_invalid_reason_code(&error); - invalid_receipts.push(ValidationReceiptInvalidCandidateView { - receipt_event_id: converted.id, - kind: converted.kind, - reason_code: reason_code.to_owned(), - reason: error.to_string(), - proof_verification: invalid_proof_verification_view(&error), - }); - } - } + reason_code: Some(invalid.reason_code), + reason: Some(invalid.reason), + sdk_error: None, + actions: Vec::new(), } +} - let evidence_bindings = verified_receipts - .iter() - .map(|(event, receipt, tags)| WorkerEvidenceReceiptBinding { - receipt_event_id: event.id.as_str(), - receipt, - tags, - }) +fn list_from_sdk_receipt( + sdk_receipt: TradeValidationReceiptListReceipt, +) -> ValidationReceiptListView { + let target_relays = sdk_receipt.relay_targets; + let connected_relays = connected_relays(&sdk_receipt.relay_evidence.relays); + let failed_relays = sdk_relay_failures(&sdk_receipt.relay_evidence.relays); + let mut invalid_receipts = sdk_receipt + .invalid_receipts + .into_iter() + .map(invalid_candidate_view) .collect::<Vec<_>>(); - let mut worker_evidence = worker_evidence_for_receipts(config, &evidence_bindings); let mut receipts = Vec::new(); - for (event, receipt, tags) in verified_receipts { + for sdk_event in sdk_receipt.receipts { let proof_verification = proof_verification_view_for_receipt( - &receipt, - worker_evidence - .remove(event.id.as_str()) - .unwrap_or_default(), + &sdk_event.receipt, + sdk_worker_evidence_selection(sdk_event.worker_evidence), ); if proof_state_is_invalid(proof_verification.state.as_str()) { invalid_receipts.push(ValidationReceiptInvalidCandidateView { - receipt_event_id: event.id, - kind: event.kind, + receipt_event_id: sdk_event.event.id, + kind: sdk_event.event.kind, reason_code: proof_verification .reason_code .clone() @@ -554,11 +490,15 @@ fn list_from_fetch_receipt( proof_verification: Some(proof_verification), }); } else { - receipts.push(summary_view(&event, &receipt, &tags, &proof_verification)); + receipts.push(summary_view( + &sdk_event.event, + &sdk_event.receipt, + &sdk_event.tags, + &proof_verification, + )); } } - let failed_relays = relay_failures(failed_relays); let valid_count = receipts.len(); let invalid_count = invalid_receipts.len(); let state = if valid_count > 0 && invalid_count > 0 { @@ -589,7 +529,7 @@ fn list_from_fetch_receipt( ValidationReceiptListView { state: state.to_owned(), - order_id: order_id.to_owned(), + order_id: sdk_receipt.order_id.as_str().to_owned(), count: valid_count + invalid_count, valid_count, invalid_count, @@ -600,18 +540,18 @@ fn list_from_fetch_receipt( failed_relays, reason_code, reason, + sdk_error: None, actions: Vec::new(), } } -fn inspection_fetch_error_view( +fn inspection_sdk_error_view( receipt_event_id: &str, - error: DirectRelayFetchError, + error: CliSdkAdapterError, ) -> ValidationReceiptInspectionView { - let (state, reason_code, reason, target_relays, connected_relays, failed_relays, actions) = - fetch_error_parts(error); + let mapped = validation_receipt_sdk_error_parts(error); ValidationReceiptInspectionView { - state, + state: mapped.state, resource: Some(validation_receipt_resource(receipt_event_id)), receipt_event_id: Some(receipt_event_id.to_owned()), order_id: None, @@ -620,105 +560,86 @@ fn inspection_fetch_error_view( receipt: None, receipt_tags: None, event: None, - target_relays, - connected_relays, - failed_relays, - reason_code: Some(reason_code), - reason: Some(reason), - actions, + target_relays: Vec::new(), + connected_relays: Vec::new(), + failed_relays: Vec::new(), + reason_code: Some(mapped.reason_code), + reason: Some(mapped.reason), + sdk_error: mapped.sdk_error, + actions: mapped.actions, } } -fn list_fetch_error_view( - order_id: &str, - error: DirectRelayFetchError, -) -> ValidationReceiptListView { - let (state, reason_code, reason, target_relays, connected_relays, failed_relays, actions) = - fetch_error_parts(error); +fn list_sdk_error_view(order_id: &str, error: CliSdkAdapterError) -> ValidationReceiptListView { + let mapped = validation_receipt_sdk_error_parts(error); ValidationReceiptListView { - state, + state: mapped.state, order_id: order_id.to_owned(), count: 0, valid_count: 0, invalid_count: 0, receipts: Vec::new(), invalid_receipts: Vec::new(), - target_relays, - connected_relays, - failed_relays, - reason_code: Some(reason_code), - reason: Some(reason), - actions, + target_relays: Vec::new(), + connected_relays: Vec::new(), + failed_relays: Vec::new(), + reason_code: Some(mapped.reason_code), + reason: Some(mapped.reason), + sdk_error: mapped.sdk_error, + actions: mapped.actions, } } -fn fetch_error_parts( - error: DirectRelayFetchError, -) -> ( - String, - String, - String, - Vec<String>, - Vec<String>, - Vec<RelayFailureView>, - Vec<String>, -) { +struct ValidationReceiptSdkErrorParts { + state: String, + reason_code: String, + reason: String, + sdk_error: Option<Value>, + actions: Vec<String>, +} + +fn validation_receipt_sdk_error_parts(error: CliSdkAdapterError) -> ValidationReceiptSdkErrorParts { match error { - DirectRelayFetchError::MissingRelays => ( - "unconfigured".to_owned(), - "relay_unconfigured".to_owned(), - "validation receipt commands require at least one configured relay".to_owned(), - Vec::new(), - Vec::new(), - Vec::new(), - vec![ + CliSdkAdapterError::Sdk(error) => sdk_error_parts(error), + CliSdkAdapterError::Runtime(error) => ValidationReceiptSdkErrorParts { + state: "network_unavailable".to_owned(), + reason_code: "sdk_runtime_failed".to_owned(), + reason: error.to_string(), + sdk_error: None, + actions: Vec::new(), + }, + } +} + +fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts { + let state = match error.code() { + "empty_target_relays" => "unconfigured", + _ => match error.class() { + radroots_sdk::RadrootsSdkErrorClass::Configuration + | radroots_sdk::RadrootsSdkErrorClass::Unsupported => "unconfigured", + radroots_sdk::RadrootsSdkErrorClass::Request => "invalid", + radroots_sdk::RadrootsSdkErrorClass::Transport + | radroots_sdk::RadrootsSdkErrorClass::Storage + | radroots_sdk::RadrootsSdkErrorClass::Clock + | radroots_sdk::RadrootsSdkErrorClass::Authorization + | radroots_sdk::RadrootsSdkErrorClass::LocalMutation => "network_unavailable", + _ => "network_unavailable", + }, + }; + let actions = if error.code() == "empty_target_relays" { + vec![ "radroots --relay wss://relay.example.com validation receipt list --trade-id <trade-id>" .to_owned(), - ], - ), - DirectRelayFetchError::Connect { - reason, - target_relays, - failed_relays, - } => ( - "network_unavailable".to_owned(), - "relay_fetch_failed".to_owned(), - reason, - target_relays, - Vec::new(), - relay_failures(failed_relays), - Vec::new(), - ), - DirectRelayFetchError::RelayConfig { relay, source } => ( - "network_unavailable".to_owned(), - "relay_config_failed".to_owned(), - format!("failed to configure relay `{relay}` for validation receipt fetch: {source}"), - vec![relay.clone()], - Vec::new(), - vec![RelayFailureView { - relay, - reason: source.to_string(), - }], - Vec::new(), - ), - DirectRelayFetchError::Fetch(source) => ( - "network_unavailable".to_owned(), - "relay_fetch_failed".to_owned(), - source.to_string(), - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - ), - DirectRelayFetchError::Runtime(reason) => ( - "network_unavailable".to_owned(), - "relay_fetch_runtime_failed".to_owned(), - reason, - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - ), + ] + } else { + Vec::new() + }; + ValidationReceiptSdkErrorParts { + state: state.to_owned(), + reason_code: error.code().to_owned(), + reason: error.to_string(), + sdk_error: Some(error.detail_json()), + actions, } } @@ -742,6 +663,7 @@ fn invalid_inspection_view( failed_relays: Vec::new(), reason_code: Some(reason_code.to_owned()), reason: Some(reason.into()), + sdk_error: None, actions: Vec::new(), } } @@ -764,6 +686,7 @@ fn invalid_list_view( failed_relays: Vec::new(), reason_code: Some(reason_code.to_owned()), reason: Some(reason.into()), + sdk_error: None, actions: Vec::new(), } } @@ -787,30 +710,20 @@ fn event_view(event: radroots_events::RadrootsNostrEvent) -> ValidationReceiptEv } } -fn tags_view(tags: &RadrootsValidationReceiptTags) -> ValidationReceiptTagsView { +fn tags_view(tags: &TradeValidationReceiptTags) -> ValidationReceiptTagsView { ValidationReceiptTagsView { order_id: tags.order_id.clone(), event_set_root: tags.event_set_root.clone(), listing_event_id: tags.listing_event_id.clone(), reducer_output_root: tags.reducer_output_root.clone(), public_values_hash: tags.public_values_hash.clone(), - proof_system: tags.proof_system.as_str().to_owned(), - receipt_type: receipt_type_label(tags.receipt_type).to_owned(), + proof_system: tags.proof_system.clone(), + receipt_type: tags.receipt_type.clone(), root_event_id: tags.root_event_id.clone(), target_event_id: tags.target_event_id.clone(), } } -fn proof_verification_view( - config: &RuntimeConfig, - receipt_event_id: &str, - receipt: &RadrootsTradeValidationReceipt, - tags: &RadrootsValidationReceiptTags, -) -> ValidationReceiptProofVerificationView { - let worker_evidence = worker_evidence_for_receipt(config, receipt_event_id, receipt, tags); - proof_verification_view_for_receipt(receipt, worker_evidence) -} - fn proof_verification_view_for_receipt( receipt: &RadrootsTradeValidationReceipt, worker_evidence: ValidationReceiptWorkerEvidenceSelection, @@ -945,7 +858,10 @@ fn sp1_unverified_proof_view( } } -fn validation_receipt_invalid_reason_code(error: &RadrootsValidationReceiptError) -> &'static str { +#[cfg(test)] +fn validation_receipt_invalid_reason_code( + error: &radroots_trade::validation_receipt::RadrootsValidationReceiptError, +) -> &'static str { use radroots_trade::validation_receipt::RadrootsValidationReceiptError; match error { @@ -978,67 +894,6 @@ fn validation_receipt_invalid_reason_code(error: &RadrootsValidationReceiptError } } -fn invalid_proof_verification_view( - error: &RadrootsValidationReceiptError, -) -> Option<ValidationReceiptProofVerificationView> { - let reason_code = validation_receipt_invalid_reason_code(error); - let (state, public_values_hash_binding, proof_metadata_binding) = match error { - RadrootsValidationReceiptError::InvalidProofMetadata("proof.material") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => ( - "sp1_proof_material_missing", - "unverified", - "missing_proof_material", - ), - RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => ( - "sp1_proof_material_conflict", - "unverified", - "conflicting_proof_material", - ), - RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.mode") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.program_hash") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.verifying_key_hash") - | RadrootsValidationReceiptError::InvalidProofMetadata("proof.system") - | RadrootsValidationReceiptError::TagMismatch("proof_system") - | RadrootsValidationReceiptError::ExpectedBindingMismatch("proof_system") => { - ("sp1_proof_invalid", "unverified", "invalid") - } - RadrootsValidationReceiptError::TagMismatch("public_values_hash") - | RadrootsValidationReceiptError::ExpectedBindingMismatch("public_values_hash") => ( - "sp1_public_values_mismatch", - "mismatch", - "metadata_consistent", - ), - RadrootsValidationReceiptError::ExpectedBindingMismatch("program_hash") => { - ("sp1_program_hash_mismatch", "unverified", "mismatch") - } - RadrootsValidationReceiptError::ExpectedBindingMismatch("verifying_key_hash") => { - ("sp1_verifying_key_hash_mismatch", "unverified", "mismatch") - } - _ => return None, - }; - - Some(ValidationReceiptProofVerificationView { - state: state.to_owned(), - verifier: "radroots_cli_validation_receipt_v1".to_owned(), - proof_system: "unknown".to_owned(), - public_values_hash_binding: public_values_hash_binding.to_owned(), - proof_metadata_binding: proof_metadata_binding.to_owned(), - cryptographic_proof_required: true, - cryptographic_proof_verified: false, - mode: None, - program_hash: None, - verifying_key_hash: None, - proof_reference: None, - inline_proof_present: false, - worker_evidence: None, - untrusted_worker_evidence: None, - reason_code: Some(reason_code.to_owned()), - reason: Some(error.to_string()), - }) -} - struct MappedSp1ProofError { state: &'static str, public_values_hash_binding: &'static str, @@ -1142,167 +997,82 @@ fn proof_state_is_verification_success(state: &str) -> bool { ) } -fn validation_receipt_worker_result_filter( - receipt_event_ids: Vec<String>, -) -> Result<RadrootsNostrFilter, String> { - let filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom( - KIND_TRADE_TRANSITION_PROOF_RESULT as u16, - )); - radroots_nostr_filter_tag(filter, "e", receipt_event_ids) - .map_err(|error| format!("build validation receipt worker result filter: {error}")) -} - -struct WorkerEvidenceReceiptBinding<'a> { - receipt_event_id: &'a str, - receipt: &'a RadrootsTradeValidationReceipt, - tags: &'a RadrootsValidationReceiptTags, +fn invalid_candidate_view( + candidate: TradeValidationReceiptInvalidCandidate, +) -> ValidationReceiptInvalidCandidateView { + ValidationReceiptInvalidCandidateView { + receipt_event_id: candidate.event.id, + kind: candidate.event.kind, + reason_code: candidate.reason_code, + reason: candidate.reason, + proof_verification: None, + } } -fn worker_evidence_for_receipt( - config: &RuntimeConfig, - receipt_event_id: &str, - receipt: &RadrootsTradeValidationReceipt, - tags: &RadrootsValidationReceiptTags, +fn sdk_worker_evidence_selection( + selection: SdkWorkerEvidenceSelection, ) -> ValidationReceiptWorkerEvidenceSelection { - let bindings = [WorkerEvidenceReceiptBinding { - receipt_event_id, - receipt, - tags, - }]; - worker_evidence_for_receipts(config, &bindings) - .remove(receipt_event_id) - .unwrap_or_default() + ValidationReceiptWorkerEvidenceSelection { + trusted: selection.trusted.map(worker_evidence_view), + untrusted: selection.untrusted.map(worker_evidence_view), + } } -fn worker_evidence_for_receipts( - config: &RuntimeConfig, - bindings: &[WorkerEvidenceReceiptBinding<'_>], -) -> BTreeMap<String, ValidationReceiptWorkerEvidenceSelection> { - if config.rhi.trusted_worker_pubkeys.is_empty() || bindings.is_empty() { - return BTreeMap::new(); +fn worker_evidence_view( + evidence: TradeValidationReceiptWorkerEvidence, +) -> ValidationReceiptWorkerEvidenceView { + ValidationReceiptWorkerEvidenceView { + result_event_id: evidence.result_event_id.as_str().to_owned(), + author: evidence.author.as_str().to_owned(), + status: evidence.status, + prover_backend: evidence.prover_backend, + proof_mode: evidence.proof_mode, + proof_system: evidence.proof_system, + proof_generated: evidence.proof_generated, + sp1_execute_checked: evidence.sp1_execute_checked, + sp1_execute_public_values_hash: evidence.sp1_execute_public_values_hash, + cryptographic_proof_verified: evidence.cryptographic_proof_verified, + public_values_hash: evidence.public_values_hash, } - let receipt_event_ids = bindings - .iter() - .map(|binding| binding.receipt_event_id.to_owned()) - .collect::<Vec<_>>(); - let filter = match validation_receipt_worker_result_filter(receipt_event_ids) { - Ok(filter) => filter, - Err(_) => return BTreeMap::new(), - }; - let fetch_receipt = match fetch_events_from_relays(&config.relay.urls, filter) { - Ok(fetch_receipt) => fetch_receipt, - Err(_) => return BTreeMap::new(), - }; - let binding_by_receipt_id = bindings - .iter() - .map(|binding| (binding.receipt_event_id, binding)) - .collect::<BTreeMap<_, _>>(); - let trusted_pubkeys = config - .rhi - .trusted_worker_pubkeys +} + +fn connected_relays(relays: &[TradeValidationReceiptRelayOutcomeReceipt]) -> Vec<String> { + relays .iter() - .map(|pubkey| pubkey.to_ascii_lowercase()) - .collect::<BTreeSet<_>>(); - let mut by_receipt = - BTreeMap::<String, Vec<(u64, String, bool, ValidationReceiptWorkerEvidenceView)>>::new(); - - for event in fetch_receipt.events { - let payload = - match serde_json::from_str::<RawValidationReceiptWorkerResultPayload>(&event.content) { - Ok(payload) => payload, - Err(_) => continue, - }; - let Some(binding) = binding_by_receipt_id.get(payload.receipt_event_id.as_str()) else { - continue; - }; - let converted = radroots_event_from_nostr(&event); - let author = converted.author.to_ascii_lowercase(); - let trusted_author = trusted_pubkeys.contains(author.as_str()); - let typed_payload = payload.typed(); - let bound = typed_payload - .as_ref() - .is_some_and(|payload| worker_payload_binds_receipt(payload, binding)); - let trusted = trusted_author && bound; - let receipt_event_id = payload.receipt_event_id.clone(); - let result_event_id = event.id.to_hex(); - let view = ValidationReceiptWorkerEvidenceView { - result_event_id: result_event_id.clone(), - author, - status: payload.status, - prover_backend: payload.prover_backend, - proof_mode: payload.proof_mode, - proof_system: payload.proof_system, - proof_generated: payload.proof_generated, - sp1_execute_checked: payload.sp1_execute_checked, - sp1_execute_public_values_hash: payload.sp1_execute_public_values_hash, - cryptographic_proof_verified: payload.cryptographic_proof_verified, - public_values_hash: payload.public_values_hash, - }; - by_receipt.entry(receipt_event_id).or_default().push(( - event.created_at.as_secs(), - result_event_id, - trusted, - view, - )); - } + .filter(|relay| relay.outcome_kind == TradeValidationReceiptRelayOutcomeKind::Eose) + .map(|relay| relay.relay_url.clone()) + .collect() +} - by_receipt - .into_iter() - .map(|(receipt_event_id, mut candidates)| { - candidates.sort_by(|left, right| { - left.0 - .cmp(&right.0) - .then_with(|| left.1.cmp(&right.1)) - .then_with(|| left.2.cmp(&right.2)) - }); - let mut selection = ValidationReceiptWorkerEvidenceSelection::default(); - for (_, _, trusted, view) in candidates.into_iter().rev() { - if trusted && selection.trusted.is_none() { - selection.trusted = Some(view); - } else if !trusted && selection.untrusted.is_none() { - selection.untrusted = Some(view); - } - if selection.trusted.is_some() && selection.untrusted.is_some() { - break; - } - } - (receipt_event_id, selection) +fn sdk_relay_failures( + relays: &[TradeValidationReceiptRelayOutcomeReceipt], +) -> Vec<RelayFailureView> { + relays + .iter() + .filter(|relay| relay.outcome_kind != TradeValidationReceiptRelayOutcomeKind::Eose) + .map(|relay| RelayFailureView { + relay: relay.relay_url.clone(), + reason: relay + .message + .clone() + .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()), }) .collect() } -fn worker_payload_binds_receipt( - payload: &RadrootsSp1TradeWorkerResultPayload, - binding: &WorkerEvidenceReceiptBinding<'_>, -) -> bool { - let receipt = binding.receipt; - let tags = binding.tags; - payload.status == RadrootsSp1TradeWorkerResultStatus::Succeeded - && payload.worker_role == Some(RadrootsSp1TradeWorkerRole::NonAuthoritativeProver) - && payload.receipt_kind == Some(KIND_TRADE_VALIDATION_RECEIPT) - && payload.receipt_event_id == binding.receipt_event_id - && payload.order_id.as_deref() == Some(tags.order_id.as_str()) - && payload.listing_event_id.as_deref() == Some(tags.listing_event_id.as_str()) - && payload.event_set_root.as_deref() == Some(tags.event_set_root.as_str()) - && payload.reducer_output_root.as_deref() == Some(tags.reducer_output_root.as_str()) - && payload.request_event_id.as_deref() == Some(tags.root_event_id.as_str()) - && payload.decision_event_id.as_deref() == Some(tags.target_event_id.as_str()) - && payload.public_values_hash == receipt.public_values_hash - && payload.proof_system == receipt.proof.system - && payload.proof_mode.mode_label().unwrap_or("none") - == receipt.proof.mode.as_deref().unwrap_or("none") - && payload.proof_generated - == (receipt.proof.system != RadrootsValidationReceiptProofSystem::None) - && payload.cryptographic_proof_verified == payload.proof_generated - && payload.sp1_execute_checked - && payload.sp1_execute_public_values_hash.as_deref() - == Some(receipt.public_values_hash.as_str()) +fn sdk_relay_outcome_kind(kind: TradeValidationReceiptRelayOutcomeKind) -> &'static str { + match kind { + TradeValidationReceiptRelayOutcomeKind::Eose => "eose", + TradeValidationReceiptRelayOutcomeKind::Closed => "closed", + TradeValidationReceiptRelayOutcomeKind::Notice => "notice", + _ => "unknown", + } } fn summary_view( event: &radroots_events::RadrootsNostrEvent, receipt: &RadrootsTradeValidationReceipt, - tags: &RadrootsValidationReceiptTags, + tags: &TradeValidationReceiptTags, proof_verification: &ValidationReceiptProofVerificationView, ) -> ValidationReceiptSummaryView { ValidationReceiptSummaryView { @@ -1332,32 +1102,19 @@ fn receipt_result_label(value: RadrootsValidationReceiptResult) -> &'static str } } -fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> { - failures - .into_iter() - .map(|failure| RelayFailureView { - relay: failure.relay, - reason: failure.reason, - }) - .collect() -} - #[cfg(test)] mod tests { use super::{ - RawValidationReceiptWorkerResultPayload, ValidationReceiptWorkerEvidenceSelection, - ValidationReceiptWorkerEvidenceView, WorkerEvidenceReceiptBinding, + ValidationReceiptWorkerEvidenceSelection, ValidationReceiptWorkerEvidenceView, proof_state_from_sp1_error, proof_state_is_invalid, proof_verification_view_for_receipt, - validation_receipt_invalid_reason_code, worker_payload_binds_receipt, + validation_receipt_invalid_reason_code, }; - use radroots_events::kinds::KIND_TRADE_VALIDATION_RECEIPT; use radroots_sp1_host_trade::RadrootsSp1TradeHostError; use radroots_trade::validation_receipt::{ RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, - RadrootsValidationReceiptTags, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, - VALIDATION_RECEIPT_VERSION, + RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, VALIDATION_RECEIPT_VERSION, }; fn sp1_proof_with_material() -> RadrootsValidationReceiptProof { @@ -1416,109 +1173,6 @@ mod tests { }) } - fn receipt_tags() -> RadrootsValidationReceiptTags { - RadrootsValidationReceiptTags { - event_set_root: "0x2222222222222222222222222222222222222222222222222222222222222222" - .to_owned(), - listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_owned(), - order_id: "order-1".to_owned(), - proof_system: RadrootsValidationReceiptProofSystem::None, - public_values_hash: - "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), - receipt_type: RadrootsValidationReceiptType::TradeTransition, - reducer_output_root: - "0x3333333333333333333333333333333333333333333333333333333333333333".to_owned(), - root_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_owned(), - target_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_owned(), - } - } - - fn worker_result_payload(listing_event_id: &str) -> RawValidationReceiptWorkerResultPayload { - RawValidationReceiptWorkerResultPayload { - cryptographic_proof_verified: false, - decision_event_id: Some( - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_owned(), - ), - event_set_root: Some( - "0x2222222222222222222222222222222222222222222222222222222222222222".to_owned(), - ), - listing_event_id: Some(listing_event_id.to_owned()), - order_id: Some("order-1".to_owned()), - proof_generated: false, - proof_mode: "none".to_owned(), - proof_system: "none".to_owned(), - public_values_hash: - "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), - prover_backend: "local_execute".to_owned(), - receipt_kind: Some(KIND_TRADE_VALIDATION_RECEIPT), - receipt_event_id: "receipt-1".to_owned(), - reducer_output_root: Some( - "0x3333333333333333333333333333333333333333333333333333333333333333".to_owned(), - ), - request_event_id: Some( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(), - ), - sp1_execute_checked: true, - sp1_execute_public_values_hash: Some( - "0x5555555555555555555555555555555555555555555555555555555555555555".to_owned(), - ), - status: "succeeded".to_owned(), - worker_role: Some("non_authoritative_prover".to_owned()), - } - } - - #[test] - fn worker_evidence_binds_distinct_listing_request_and_decision_ids() { - let receipt = deterministic_receipt(); - let tags = receipt_tags(); - let binding = WorkerEvidenceReceiptBinding { - receipt_event_id: "receipt-1", - receipt: &receipt, - tags: &tags, - }; - - assert!(worker_payload_binds_receipt( - &worker_result_payload( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ) - .typed() - .expect("typed payload"), - &binding - )); - } - - #[test] - fn worker_evidence_rejects_listing_id_mismatch() { - let receipt = deterministic_receipt(); - let tags = receipt_tags(); - let binding = WorkerEvidenceReceiptBinding { - receipt_event_id: "receipt-1", - receipt: &receipt, - tags: &tags, - }; - - assert!(!worker_payload_binds_receipt( - &worker_result_payload( - "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - ) - .typed() - .expect("typed payload"), - &binding - )); - } - - #[test] - fn worker_evidence_unknown_typed_values_are_not_trusted() { - let mut payload = worker_result_payload( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ); - payload.prover_backend = "future_backend".to_owned(); - assert!(payload.typed().is_none()); - } - #[test] fn none_receipts_report_deterministic_verification_without_crypto_claim() { let view = proof_verification_view_for_receipt(