lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit e0e4f14d2db5e35027235b7f5c4095884bce420d
parent 86c6fa3219fa158b888a01c50d3f8ecf5541bf0c
Author: triesap <tyson@radroots.org>
Date:   Sat,  4 Jul 2026 01:48:09 +0000

status: enforce validation trust policy

- thread trust policy through trade status and resync
- downgrade untrusted committed projections to pending RHI
- expose validation trust decisions in SDK receipts
- cover trusted local and cryptographic committed paths

Diffstat:
Mcrates/sdk/src/lib.rs | 2++
Mcrates/sdk/src/orders_runtime.rs | 539++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/sdk/tests/dvm_runtime.rs | 24++++++++++++++++++------
Mcrates/sdk/tests/orders_runtime.rs | 324++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/sdk/tests/source_boundary.rs | 3+++
5 files changed, 857 insertions(+), 35 deletions(-)

diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -120,6 +120,7 @@ pub use crate::orders_runtime::{ TradeValidationReceiptRelayOutcomeReceipt, TradeValidationReceiptRelayTransportOutcomeKind, TradeValidationReceiptTags, TradeValidationReceiptVerifyRequest, TradeValidationReceiptWorkerEvidence, TradeValidationReceiptWorkerEvidenceSelection, + TradeValidationTrustDecision, }; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] pub use crate::orders_runtime::{ @@ -190,4 +191,5 @@ pub use radroots_trade::dvm::RadrootsTradeInventoryBinWitnessDto; #[cfg(feature = "runtime")] pub use radroots_trade::validation_receipt::{ RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority, + RadrootsTradeValidationTrustPolicy, RadrootsTradeValidationTrustState, }; diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -75,6 +75,8 @@ use radroots_relay_transport::{ RadrootsRelayFetchRequest, RadrootsRelayOutcomeKind, fetch_and_ingest_relay_events, }; #[cfg(feature = "runtime")] +use radroots_trade::dvm::RADROOTS_DVM_TAG_VALIDATION_RECEIPT; +#[cfg(feature = "runtime")] use radroots_trade::identity::{RadrootsTradeLocator, RadrootsTradeLocatorCandidate}; #[cfg(feature = "runtime")] use radroots_trade::listing::parse_listing_address; @@ -94,9 +96,11 @@ use radroots_trade::order::{ #[cfg(feature = "runtime")] use radroots_trade::validation_receipt::{ RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority, - RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, + RadrootsTradeValidationReceipt, RadrootsTradeValidationTrustPolicy, + RadrootsTradeValidationTrustState, RadrootsValidationReceiptError, RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem, - RadrootsValidationReceiptTags, verify_validation_receipt_event, + RadrootsValidationReceiptResult, RadrootsValidationReceiptTags, + verify_validation_receipt_event, }; #[cfg(feature = "runtime")] use radroots_trade::workflow::RadrootsTradeWorkflowState; @@ -1480,6 +1484,7 @@ pub struct TradeSellerInboxReceipt { pub struct TradeResyncRequest { pub locator: RadrootsTradeLocator, pub limit: u32, + pub validation_trust_policy: RadrootsTradeValidationTrustPolicy, } #[cfg(feature = "runtime")] @@ -1488,6 +1493,7 @@ impl TradeResyncRequest { Self { locator, limit: TRADE_STATUS_DEFAULT_LIMIT, + validation_trust_policy: RadrootsTradeValidationTrustPolicy::production(), } } @@ -1496,6 +1502,26 @@ impl TradeResyncRequest { self } + pub fn with_validation_trust_policy( + mut self, + policy: RadrootsTradeValidationTrustPolicy, + ) -> Self { + self.validation_trust_policy = policy; + self + } + + pub fn try_with_trusted_rhi_pubkeys<I, S>( + mut self, + pubkeys: I, + ) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + self.validation_trust_policy.trusted_rhi_pubkeys = parse_worker_pubkeys(pubkeys)?; + Ok(self) + } + fn validate(&self) -> Result<(), RadrootsSdkError> { if self.limit == 0 || self.limit > TRADE_STATUS_MAX_LIMIT { return Err(RadrootsSdkError::trade_status_limit_invalid( @@ -1925,6 +1951,7 @@ pub struct TradeStatusRequest { pub locator: RadrootsTradeLocator, pub limit: u32, pub source: SdkTradeStatusSource, + pub validation_trust_policy: RadrootsTradeValidationTrustPolicy, } #[cfg(feature = "runtime")] @@ -1934,6 +1961,7 @@ impl TradeStatusRequest { locator, limit: TRADE_STATUS_DEFAULT_LIMIT, source: SdkTradeStatusSource::LocalOnly, + validation_trust_policy: RadrootsTradeValidationTrustPolicy::production(), } } @@ -1954,6 +1982,26 @@ impl TradeStatusRequest { self } + pub fn with_validation_trust_policy( + mut self, + policy: RadrootsTradeValidationTrustPolicy, + ) -> Self { + self.validation_trust_policy = policy; + self + } + + pub fn try_with_trusted_rhi_pubkeys<I, S>( + mut self, + pubkeys: I, + ) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + self.validation_trust_policy.trusted_rhi_pubkeys = parse_worker_pubkeys(pubkeys)?; + Ok(self) + } + fn validate(&self) -> Result<(), RadrootsSdkError> { if self.limit == 0 || self.limit > TRADE_STATUS_MAX_LIMIT { return Err(RadrootsSdkError::trade_status_limit_invalid( @@ -1984,6 +2032,7 @@ pub struct TradeStatusReceipt { pub seller_pubkey: Option<RadrootsPublicKey>, pub economics: Option<RadrootsOrderEconomics>, pub evidence: TradeStatusEvidenceSummary, + pub validation_trust: Option<TradeValidationTrustDecision>, pub online_evidence: Option<TradeResyncEvidenceReceipt>, pub eligibility: TradeStatusEligibility, pub next_action: TradeStatusNextActionKind, @@ -2012,6 +2061,7 @@ pub struct TradeStatusEvidenceSummary { pub has_request: bool, pub has_decision: bool, pub has_agreement: bool, + pub has_validation_receipt: bool, pub has_pending_revision: bool, pub has_cancellation: bool, pub has_issues: bool, @@ -2019,6 +2069,27 @@ pub struct TradeStatusEvidenceSummary { #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct TradeValidationTrustDecision { + pub state: RadrootsTradeValidationTrustState, + pub trusted_rhi_pubkey_count: usize, + pub allow_deterministic_none: bool, + pub require_cryptographic_proof: bool, + pub receipt_event_id: Option<RadrootsEventId>, + pub receipt_author: Option<RadrootsPublicKey>, + pub result_event_id: Option<RadrootsEventId>, + pub result_author: Option<RadrootsPublicKey>, + pub proof_system: Option<String>, + pub validation_authority: Option<RadrootsTradeValidationAuthority>, + pub commitment_confidence: Option<RadrootsTradeCommitmentConfidence>, + pub cryptographic_proof_required: bool, + pub cryptographic_proof_verified: bool, + pub production_committed: bool, + pub reason_code: Option<String>, + pub reason: Option<String>, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct TradeStatusEligibility { pub can_decide: bool, pub can_propose_revision: bool, @@ -3026,7 +3097,9 @@ impl<'sdk> TradesClient<'sdk> { .await?; let mut status = self .local_status( - TradeStatusRequest::new(request.locator).with_limit(request.limit), + TradeStatusRequest::new(request.locator) + .with_limit(request.limit) + .with_validation_trust_policy(request.validation_trust_policy), ) .await?; status.source = SdkTradeStatusSource::ResyncThenLocal; @@ -3051,6 +3124,12 @@ impl<'sdk> TradesClient<'sdk> { let mut receipt = TradeStatusReceipt::from_locator_query_result(request.locator, query_result); receipt.source = request.source; + apply_trade_status_validation_trust( + self.sdk, + &mut receipt, + &request.validation_trust_policy, + ) + .await?; Ok(receipt) } @@ -3170,7 +3249,11 @@ impl<'sdk> TradeResyncClient<'sdk> { ) .await?; let status = trades_client(self.sdk) - .status(TradeStatusRequest::new(request.locator.clone()).with_limit(request.limit)) + .status( + TradeStatusRequest::new(request.locator.clone()) + .with_limit(request.limit) + .with_validation_trust_policy(request.validation_trust_policy.clone()), + ) .await?; if status.status == TradeStatusKind::Ambiguous { return Err(RadrootsSdkError::TradeAmbiguous { @@ -4219,6 +4302,452 @@ fn worker_payload_execution_binds_receipt( } #[cfg(feature = "runtime")] +async fn apply_trade_status_validation_trust( + sdk: &crate::RadrootsClient, + status: &mut TradeStatusReceipt, + policy: &RadrootsTradeValidationTrustPolicy, +) -> Result<(), RadrootsSdkError> { + if !status.found + || matches!( + status.status, + TradeStatusKind::Missing | TradeStatusKind::Ambiguous + ) + { + return Ok(()); + } + let decision = trade_status_validation_trust_decision(sdk, status, policy).await?; + status.validation_trust = Some(decision); + apply_validation_trust_decision_to_status(status); + Ok(()) +} + +#[cfg(feature = "runtime")] +async fn trade_status_validation_trust_decision( + sdk: &crate::RadrootsClient, + status: &TradeStatusReceipt, + policy: &RadrootsTradeValidationTrustPolicy, +) -> Result<TradeValidationTrustDecision, RadrootsSdkError> { + let Some(receipt_event_id) = status.rhi_receipt_event_id.clone() else { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + None, + None, + None, + None, + false, + Some("validation_receipt_missing"), + Some("validation receipt is not present in local product evidence"), + )); + }; + let Some(stored_event) = sdk + ._event_store + .get_event(receipt_event_id.as_str()) + .await + .map_err(|error| RadrootsSdkError::EventStore { + message: error.to_string(), + })? + else { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + None, + None, + None, + false, + Some("validation_receipt_event_missing"), + Some("validation receipt event is referenced by projection but missing from storage"), + )); + }; + if stored_event.kind != KIND_TRADE_VALIDATION_RECEIPT { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Invalid, + Some(receipt_event_id), + None, + None, + None, + false, + Some("validation_receipt_event_kind_invalid"), + Some("validation receipt reference does not point to a validation receipt event"), + )); + } + let event = stored_event_to_nostr_event(&stored_event)?; + let receipt_author = match RadrootsPublicKey::parse(event.author.as_str()) { + Ok(author) => author, + Err(_) => { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Invalid, + Some(receipt_event_id), + None, + None, + None, + false, + Some("validation_receipt_author_invalid"), + Some("validation receipt author is not a valid public key"), + )); + } + }; + let expected = RadrootsValidationReceiptExpectedBinding { + order_id: Some(status.order_id.as_str()), + root_event_id: status + .request_event_id + .as_ref() + .map(RadrootsEventId::as_str), + target_event_id: status + .agreement_event_id + .as_ref() + .or(status.decision_event_id.as_ref()) + .map(RadrootsEventId::as_str), + ..RadrootsValidationReceiptExpectedBinding::default() + }; + let verified = match verify_validation_receipt_event(&event, expected) { + Ok(verified) => verified, + Err(error) => { + let reason = error.to_string(); + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Invalid, + Some(receipt_event_id), + Some(receipt_author), + None, + None, + false, + Some("validation_receipt_invalid"), + Some(reason.as_str()), + )); + } + }; + let receipt = TradeValidationReceiptEvent { + event, + receipt: verified.receipt, + tags: TradeValidationReceiptTags::from(verified.tags), + worker_evidence: TradeValidationReceiptWorkerEvidenceSelection::default(), + }; + if receipt.receipt.result == RadrootsValidationReceiptResult::Invalid { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Invalid, + Some(receipt_event_id), + Some(receipt_author), + Some(&receipt.receipt), + None, + false, + Some("validation_receipt_result_invalid"), + Some("validation receipt reports an invalid trade transition"), + )); + } + if policy.trusted_rhi_pubkeys.is_empty() { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Untrusted, + Some(receipt_event_id), + Some(receipt_author), + Some(&receipt.receipt), + None, + false, + Some("validation_trust_policy_empty"), + Some("validation trust policy has no trusted RHI public keys"), + )); + } + if !policy.trusts_rhi_pubkey(&receipt_author) { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Untrusted, + Some(receipt_event_id), + Some(receipt_author), + Some(&receipt.receipt), + None, + false, + Some("validation_receipt_author_untrusted"), + Some("validation receipt author is not trusted by the active policy"), + )); + } + let result_events = + validation_receipt_worker_result_events_for_receipt(sdk, &receipt_event_id).await?; + let mut selections = worker_evidence_for_receipts( + &policy.trusted_rhi_pubkeys, + core::slice::from_ref(&receipt), + result_events, + )?; + let selection = selections + .remove(receipt_event_id.as_str()) + .unwrap_or_default(); + let Some(evidence) = selection.trusted.as_ref() else { + if selection.untrusted.is_some() { + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Untrusted, + Some(receipt_event_id), + Some(receipt_author), + Some(&receipt.receipt), + selection.untrusted.as_ref(), + false, + Some("validation_result_author_untrusted"), + Some("validation result metadata was produced by an untrusted RHI public key"), + )); + } + return Ok(trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(&receipt.receipt), + None, + false, + Some("validation_result_metadata_missing"), + Some("trusted validation result metadata is not present in local product evidence"), + )); + }; + Ok(evaluate_trade_validation_trust_evidence( + policy, + receipt_event_id, + receipt_author, + &receipt.receipt, + evidence, + )) +} + +#[cfg(feature = "runtime")] +async fn validation_receipt_worker_result_events_for_receipt( + sdk: &crate::RadrootsClient, + receipt_event_id: &RadrootsEventId, +) -> Result<Vec<RadrootsNostrEvent>, RadrootsSdkError> { + let events = sdk + ._event_store + .events_by_tag( + RADROOTS_DVM_TAG_VALIDATION_RECEIPT, + receipt_event_id.as_str(), + TRADE_STATUS_MAX_LIMIT, + ) + .await + .map_err(|error| RadrootsSdkError::EventStore { + message: error.to_string(), + })?; + events + .into_iter() + .filter(|event| event.kind == KIND_TRADE_TRANSITION_PROOF_RESULT) + .map(|event| stored_event_to_nostr_event(&event)) + .collect() +} + +#[cfg(feature = "runtime")] +fn evaluate_trade_validation_trust_evidence( + policy: &RadrootsTradeValidationTrustPolicy, + receipt_event_id: RadrootsEventId, + receipt_author: RadrootsPublicKey, + receipt: &RadrootsTradeValidationReceipt, + evidence: &TradeValidationReceiptWorkerEvidence, +) -> TradeValidationTrustDecision { + let Some(authority) = evidence.validation_authority else { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("validation_authority_missing"), + Some("trusted validation result metadata does not declare validation authority"), + ); + }; + let Some(confidence) = evidence.commitment_confidence else { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("commitment_confidence_missing"), + Some("trusted validation result metadata does not declare commitment confidence"), + ); + }; + if authority == RadrootsTradeValidationAuthority::DevDeterministicOnly + || confidence == RadrootsTradeCommitmentConfidence::LocalOnly + { + if !policy.allow_deterministic_none { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Untrusted, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("deterministic_none_not_allowed"), + Some("deterministic-none validation is not allowed by the active policy"), + ); + } + if policy.require_cryptographic_proof { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("cryptographic_proof_required"), + Some("active policy requires cryptographic proof for committed confidence"), + ); + } + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::TrustedLocal, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + None, + None, + ); + } + let cryptographic_metadata = matches!( + (authority, confidence), + ( + RadrootsTradeValidationAuthority::CryptographicProofVerified, + RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof + ) | ( + RadrootsTradeValidationAuthority::TrustedServiceAndProofVerified, + RadrootsTradeCommitmentConfidence::CommittedByTrustedServiceAndProof + ) + ); + if cryptographic_metadata + && receipt.proof.system != RadrootsValidationReceiptProofSystem::None + && evidence.cryptographic_proof_verified + { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::CryptographicCommitted, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + true, + None, + None, + ); + } + if policy.require_cryptographic_proof { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("cryptographic_proof_required"), + Some("active policy requires trusted cryptographic proof metadata"), + ); + } + if matches!( + (authority, confidence), + ( + RadrootsTradeValidationAuthority::TrustedRhiServiceKey, + RadrootsTradeCommitmentConfidence::CommittedByTrustedService + ) | ( + RadrootsTradeValidationAuthority::TrustedServiceAndProofVerified, + RadrootsTradeCommitmentConfidence::CommittedByTrustedServiceAndProof + ) | ( + RadrootsTradeValidationAuthority::CryptographicProofVerified, + RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof + ) + ) { + return trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::TrustedLocal, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + None, + None, + ); + } + trade_validation_trust_decision( + policy, + RadrootsTradeValidationTrustState::Pending, + Some(receipt_event_id), + Some(receipt_author), + Some(receipt), + Some(evidence), + false, + Some("validation_trust_metadata_insufficient"), + Some("trusted validation result metadata does not satisfy the active policy"), + ) +} + +#[cfg(feature = "runtime")] +fn trade_validation_trust_decision( + policy: &RadrootsTradeValidationTrustPolicy, + state: RadrootsTradeValidationTrustState, + receipt_event_id: Option<RadrootsEventId>, + receipt_author: Option<RadrootsPublicKey>, + receipt: Option<&RadrootsTradeValidationReceipt>, + evidence: Option<&TradeValidationReceiptWorkerEvidence>, + production_committed: bool, + reason_code: Option<&str>, + reason: Option<&str>, +) -> TradeValidationTrustDecision { + TradeValidationTrustDecision { + state, + trusted_rhi_pubkey_count: policy.trusted_rhi_pubkey_count(), + allow_deterministic_none: policy.allow_deterministic_none, + require_cryptographic_proof: policy.require_cryptographic_proof, + receipt_event_id, + receipt_author, + result_event_id: evidence.map(|evidence| evidence.result_event_id.clone()), + result_author: evidence.map(|evidence| evidence.author.clone()), + proof_system: receipt.map(|receipt| receipt.proof.system.as_str().to_owned()), + validation_authority: evidence.and_then(|evidence| evidence.validation_authority), + commitment_confidence: evidence.and_then(|evidence| evidence.commitment_confidence), + cryptographic_proof_required: policy.require_cryptographic_proof, + cryptographic_proof_verified: evidence + .is_some_and(|evidence| evidence.cryptographic_proof_verified), + production_committed, + reason_code: reason_code.map(str::to_owned), + reason: reason.map(str::to_owned), + } +} + +#[cfg(feature = "runtime")] +fn apply_validation_trust_decision_to_status(status: &mut TradeStatusReceipt) { + let Some(decision) = status.validation_trust.as_ref() else { + return; + }; + match decision.state { + RadrootsTradeValidationTrustState::Pending + | RadrootsTradeValidationTrustState::Untrusted => { + if status.status == TradeStatusKind::Committed { + status.status = TradeStatusKind::AgreedPendingRhi; + status.lifecycle_terminal = false; + status.next_action = TradeStatusNextActionKind::AwaitRhiValidation; + status.last_event_id = status + .agreement_event_id + .clone() + .or_else(|| status.decision_event_id.clone()) + .or_else(|| status.request_event_id.clone()); + } + } + RadrootsTradeValidationTrustState::Invalid => { + status.status = TradeStatusKind::Invalid; + status.lifecycle_terminal = true; + status.next_action = TradeStatusNextActionKind::InspectEvidenceIssues; + } + RadrootsTradeValidationTrustState::TrustedLocal + | RadrootsTradeValidationTrustState::CryptographicCommitted => {} + } +} + +#[cfg(feature = "runtime")] #[derive(Debug, Deserialize)] struct RawTradeValidationReceiptWorkerResult { cryptographic_proof_verified: bool, @@ -5579,6 +6108,7 @@ impl TradeStatusReceipt { seller_pubkey: projection.seller_pubkey, economics: projection.economics, evidence, + validation_trust: None, online_evidence: None, eligibility, next_action, @@ -5663,6 +6193,7 @@ impl TradeStatusEvidenceSummary { has_request: projection.request_event_id.is_some(), has_decision: projection.decision_event_id.is_some(), has_agreement: projection.agreement_event_id.is_some(), + has_validation_receipt: projection.validation_receipt_event_id.is_some(), has_pending_revision: projection.pending_revision_event_id.is_some(), has_cancellation: projection.cancellation_event_id.is_some(), has_issues: !projection.issues.is_empty(), diff --git a/crates/sdk/tests/dvm_runtime.rs b/crates/sdk/tests/dvm_runtime.rs @@ -31,8 +31,9 @@ use radroots_sdk::{ DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND, DvmTradeTransitionProofEnqueueRequest, DvmTradeTransitionProofPrepareRequest, DvmValidationReceiptIngestRequest, RadrootsClient, RadrootsSdkError, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, - RadrootsTradeInventoryBinWitnessDto, SdkMutationState, SdkRelayTargetPolicy, SdkRelayUrlPolicy, - TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, + RadrootsTradeInventoryBinWitnessDto, RadrootsTradeValidationTrustState, SdkMutationState, + SdkRelayTargetPolicy, SdkRelayUrlPolicy, TradeStatusKind, TradeStatusNextActionKind, + TradeStatusRequest, }; #[cfg(feature = "signer-adapters")] use radroots_sdk::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; @@ -329,14 +330,25 @@ async fn dvm_validation_receipt_ingest_commits_pending_trade_status() { .status(status_request("order-dvm-ingest")) .await .expect("committed status"); - assert_eq!(committed.status, TradeStatusKind::Committed); - assert!(committed.lifecycle_terminal); - assert_eq!(committed.next_action, TradeStatusNextActionKind::Terminal); + assert_eq!(committed.status, TradeStatusKind::AgreedPendingRhi); + assert!(!committed.lifecycle_terminal); + assert_eq!( + committed.next_action, + TradeStatusNextActionKind::AwaitRhiValidation + ); assert_eq!( committed.rhi_receipt_event_id, Some(receipt_event_id.clone()) ); - assert_eq!(committed.last_event_id, Some(receipt_event_id)); + assert_eq!(committed.last_event_id, Some(decision_event_id)); + let trust = committed.validation_trust.expect("validation trust"); + assert_eq!(trust.state, RadrootsTradeValidationTrustState::Untrusted); + assert_eq!( + trust.reason_code.as_deref(), + Some("validation_trust_policy_empty") + ); + assert!(!trust.production_committed); + assert_eq!(trust.proof_system.as_deref(), Some("none")); } #[tokio::test] diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -45,17 +45,17 @@ use radroots_relay_transport::{ }; use radroots_sdk::{ AckPolicy, DvmValidationReceiptIngestRequest, PublishMode, RadrootsClient, RadrootsSdkError, - RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, RelayResolutionPolicy, SdkMutationState, - SdkRelayTargetSet, SdkRelayUrlPolicy, SdkTradeStatusIssue, SdkTradeStatusIssueKind, - SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT, TRADE_STATUS_MAX_LIMIT, - TRADE_SUBMIT_OPERATION_KIND, TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest, - TradeEvidenceIngestRequest, TradeEvidenceMode, TradeEvidenceQueryBranchKind, - TradeMutationOutcome, TradeProposeRequest, TradeRequestEvidenceIngestRequest, - TradeResyncRelayOutcomeKind, TradeResyncRelayTransportOutcomeKind, TradeResyncRequest, - TradeRevisionDecisionRequest, TradeRevisionProposalRequest, TradeSellerInboxRequest, - TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, - TradeValidationReceiptInspectRequest, TradeValidationReceiptListRequest, - TradeValidationReceiptVerifyRequest, + RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, RadrootsTradeValidationTrustPolicy, + RadrootsTradeValidationTrustState, RelayResolutionPolicy, SdkMutationState, SdkRelayTargetSet, + SdkRelayUrlPolicy, SdkTradeStatusIssue, SdkTradeStatusIssueKind, SdkTradeStatusSource, + TRADE_STATUS_DEFAULT_LIMIT, TRADE_STATUS_MAX_LIMIT, TRADE_SUBMIT_OPERATION_KIND, + TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest, TradeEvidenceIngestRequest, + TradeEvidenceMode, TradeEvidenceQueryBranchKind, TradeMutationOutcome, TradeProposeRequest, + TradeRequestEvidenceIngestRequest, TradeResyncRelayOutcomeKind, + TradeResyncRelayTransportOutcomeKind, TradeResyncRequest, TradeRevisionDecisionRequest, + TradeRevisionProposalRequest, TradeSellerInboxRequest, TradeStatusKind, + TradeStatusNextActionKind, TradeStatusRequest, TradeValidationReceiptInspectRequest, + TradeValidationReceiptListRequest, TradeValidationReceiptVerifyRequest, }; use radroots_sdk::{PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField}; #[cfg(all(feature = "signer-adapters", feature = "local-signer"))] @@ -1195,6 +1195,15 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { let receipt_event = radroots_event_from_nostr(&receipt_raw_event); let receipt_event_id = RadrootsEventId::parse(receipt_raw_event.id.to_hex().as_str()).expect("receipt id"); + let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX); + let worker_raw_event = signed_raw_worker_result_event( + "trade-product-committed-resync", + &receipt_event_id, + &propose_receipt.listing_event_id, + &propose_receipt.signed_event_id, + &accept_receipt.signed_event_id, + 34, + ); let ingest = seller_sdk .dvm() @@ -1209,6 +1218,13 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { .expect("ingest validation receipt"); assert!(ingest.inserted); assert_eq!(ingest.receipt_event_id, receipt_event_id); + seller_store + .ingest_event(RadrootsEventIngest::new( + radroots_event_from_nostr(&worker_raw_event), + 4_050, + )) + .await + .expect("ingest validation result"); let seller_resync = seller_sdk .trades() @@ -1219,20 +1235,61 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { ) .await .expect("seller resync"); - assert_eq!(seller_resync.status.status, TradeStatusKind::Committed); + assert_eq!( + seller_resync.status.status, + TradeStatusKind::AgreedPendingRhi + ); assert_eq!( seller_resync.status.rhi_receipt_event_id, Some(receipt_event_id.clone()) ); assert_eq!( seller_resync.status.last_event_id, - Some(receipt_event_id.clone()) + Some(accept_receipt.signed_event_id.clone()) + ); + assert_eq!( + seller_resync + .status + .validation_trust + .as_ref() + .map(|trust| trust.state), + Some(RadrootsTradeValidationTrustState::Untrusted) + ); + + let trusted_local_policy = RadrootsTradeValidationTrustPolicy::production() + .with_trusted_rhi_pubkeys(vec![service_pubkey.parse().expect("service pubkey")]) + .with_require_cryptographic_proof(false); + let seller_trusted_local = seller_sdk + .trades() + .status( + TradeStatusRequest::new(propose_receipt.locator.clone()) + .with_validation_trust_policy(trusted_local_policy), + ) + .await + .expect("seller trusted local status"); + assert_eq!(seller_trusted_local.status, TradeStatusKind::Committed); + let seller_trust = seller_trusted_local + .validation_trust + .as_ref() + .expect("seller validation trust"); + assert_eq!( + seller_trust.state, + RadrootsTradeValidationTrustState::TrustedLocal + ); + assert!(!seller_trust.production_committed); + assert_eq!( + seller_trust + .result_author + .as_ref() + .map(|pubkey| pubkey.as_str()), + Some(service_pubkey.as_str()) ); let buyer_committed_resync_adapter = RadrootsMockRelayFetchAdapter::new(vec![ relay_event_item_from_store(&seller_store, &accept_receipt.signed_event_id, RELAY, 4_100) .await, relay_raw_event_item(&receipt_raw_event, RELAY, 4_200), + relay_raw_event_item(&worker_raw_event, RELAY, 4_201), relay_eose(RELAY), ]); let buyer_resync = buyer_sdk @@ -1244,12 +1301,100 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { ) .await .expect("buyer resync"); - assert_eq!(buyer_resync.status.status, TradeStatusKind::Committed); + assert_eq!( + buyer_resync.status.status, + TradeStatusKind::AgreedPendingRhi + ); assert_eq!( buyer_resync.status.rhi_receipt_event_id, Some(receipt_event_id) ); - assert_eq!(buyer_resync.evidence.inserted_count, 2); + assert_eq!(buyer_resync.evidence.inserted_count, 3); +} + +#[tokio::test] +async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_committed_confidence() +{ + let (_tempdir, sdk, store) = directory_sdk_and_store().await; + let order_id = "trade-status-trusted-crypto"; + let request_event = signed_order_request_event(order_id, 70); + let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); + let decision_event = signed_order_decision_event(order_id, &request_event_id, 71); + let decision_event_id = + RadrootsEventId::parse(decision_event.id.as_str()).expect("decision id"); + let listing_event_id = deterministic_event_id("listing-event"); + let receipt_raw_event = signed_raw_sp1_validation_receipt_event( + order_id, + &listing_event_id, + &request_event_id, + &decision_event_id, + 72, + ); + let receipt_event = radroots_event_from_nostr(&receipt_raw_event); + let receipt_event_id = + RadrootsEventId::parse(receipt_raw_event.id.to_hex()).expect("receipt id"); + let worker_raw_event = signed_raw_sp1_worker_result_event( + order_id, + &receipt_event_id, + &listing_event_id, + &request_event_id, + &decision_event_id, + 73, + ); + for (event, observed_at_ms) in [ + (request_event, 7_000), + (decision_event, 7_100), + (receipt_event, 7_200), + (radroots_event_from_nostr(&worker_raw_event), 7_300), + ] { + store + .ingest_event(RadrootsEventIngest::new(event, observed_at_ms)) + .await + .expect("ingest trade status trust event"); + } + + let default_status = sdk + .trades() + .status(status_request(order_id)) + .await + .expect("default status"); + assert_eq!(default_status.status, TradeStatusKind::AgreedPendingRhi); + assert_eq!( + default_status + .validation_trust + .as_ref() + .map(|trust| trust.state), + Some(RadrootsTradeValidationTrustState::Untrusted) + ); + + let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX); + let trusted_policy = RadrootsTradeValidationTrustPolicy::production() + .with_trusted_rhi_pubkeys(vec![service_pubkey.parse().expect("service pubkey")]); + let trusted_status = sdk + .trades() + .status(status_request(order_id).with_validation_trust_policy(trusted_policy)) + .await + .expect("trusted status"); + + assert_eq!(trusted_status.status, TradeStatusKind::Committed); + assert!(trusted_status.lifecycle_terminal); + assert_eq!( + trusted_status.next_action, + TradeStatusNextActionKind::Terminal + ); + let trust = trusted_status.validation_trust.expect("trusted decision"); + assert_eq!( + trust.state, + RadrootsTradeValidationTrustState::CryptographicCommitted + ); + assert!(trust.production_committed); + assert!(trust.cryptographic_proof_required); + assert!(trust.cryptographic_proof_verified); + assert_eq!(trust.proof_system.as_deref(), Some("sp1_core")); + assert_eq!( + trust.result_author.as_ref().map(|pubkey| pubkey.as_str()), + Some(service_pubkey.as_str()) + ); } #[cfg(all( @@ -3693,20 +3838,50 @@ fn signed_raw_validation_receipt_event( ) } +fn signed_raw_sp1_validation_receipt_event( + raw_order_id: &str, + listing_event_id: &RadrootsEventId, + root_event_id: &RadrootsEventId, + target_event_id: &RadrootsEventId, + created_at: u32, +) -> nostr::Event { + signed_raw_event( + SERVICE_SECRET_KEY_HEX, + created_at, + validation_receipt_wire_parts_with_proof( + raw_order_id, + listing_event_id, + root_event_id, + target_event_id, + RadrootsValidationReceiptProofSystem::Sp1Core, + ), + ) +} + fn validation_receipt_wire_parts( raw_order_id: &str, listing_event_id: &RadrootsEventId, root_event_id: &RadrootsEventId, target_event_id: &RadrootsEventId, ) -> WireEventParts { - let receipt = RadrootsTradeValidationReceipt { - changed_records_root: hash32('6'), - domain: "radroots.receipt".to_owned(), - error_bitmap: "0x00000000000000000000000000000000".to_owned(), - event_set_root: hash32('c'), - new_state_root: hash32('4'), - previous_state_root: hash32('3'), - proof: RadrootsValidationReceiptProof { + validation_receipt_wire_parts_with_proof( + raw_order_id, + listing_event_id, + root_event_id, + target_event_id, + RadrootsValidationReceiptProofSystem::None, + ) +} + +fn validation_receipt_wire_parts_with_proof( + raw_order_id: &str, + listing_event_id: &RadrootsEventId, + root_event_id: &RadrootsEventId, + target_event_id: &RadrootsEventId, + proof_system: RadrootsValidationReceiptProofSystem, +) -> WireEventParts { + let proof = match proof_system { + RadrootsValidationReceiptProofSystem::None => RadrootsValidationReceiptProof { inline_proof_base64: None, mode: None, program_hash: None, @@ -3714,6 +3889,47 @@ fn validation_receipt_wire_parts( system: RadrootsValidationReceiptProofSystem::None, verifying_key_hash: None, }, + RadrootsValidationReceiptProofSystem::Sp1Core => RadrootsValidationReceiptProof { + inline_proof_base64: Some("AQID".to_owned()), + mode: Some("core".to_owned()), + program_hash: Some(hash32('a')), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: Some(hash32('b')), + }, + RadrootsValidationReceiptProofSystem::Sp1Compressed => RadrootsValidationReceiptProof { + inline_proof_base64: Some("AQID".to_owned()), + mode: Some("compressed".to_owned()), + program_hash: Some(hash32('a')), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Compressed, + verifying_key_hash: Some(hash32('b')), + }, + RadrootsValidationReceiptProofSystem::Sp1Groth16 => RadrootsValidationReceiptProof { + inline_proof_base64: Some("AQID".to_owned()), + mode: Some("groth16".to_owned()), + program_hash: Some(hash32('a')), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Groth16, + verifying_key_hash: Some(hash32('b')), + }, + RadrootsValidationReceiptProofSystem::Sp1Plonk => RadrootsValidationReceiptProof { + inline_proof_base64: Some("AQID".to_owned()), + mode: Some("plonk".to_owned()), + program_hash: Some(hash32('a')), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Plonk, + verifying_key_hash: Some(hash32('b')), + }, + }; + let receipt = RadrootsTradeValidationReceipt { + changed_records_root: hash32('6'), + domain: "radroots.receipt".to_owned(), + error_bitmap: "0x00000000000000000000000000000000".to_owned(), + event_set_root: hash32('c'), + new_state_root: hash32('4'), + previous_state_root: hash32('3'), + proof, public_values_hash: validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#), receipt_type: RadrootsValidationReceiptType::TradeTransition, result: RadrootsValidationReceiptResult::Valid, @@ -3728,6 +3944,57 @@ fn validation_receipt_wire_parts( validation_receipt_event_build(raw_order_id, &receipt).expect("receipt event") } +fn signed_raw_sp1_worker_result_event( + raw_order_id: &str, + receipt_event_id: &RadrootsEventId, + listing_event_id: &RadrootsEventId, + root_event_id: &RadrootsEventId, + target_event_id: &RadrootsEventId, + created_at: u32, +) -> nostr::Event { + let content = serde_json::json!({ + "confidence": "committed_by_trusted_service_and_proof", + "cryptographic_proof_verified": true, + "customer_pubkey": BUYER_PUBLIC_KEY_HEX, + "decision_event_id": target_event_id.as_str(), + "event_set_root": hash32('c'), + "listing_event_id": listing_event_id.as_str(), + "order_id": raw_order_id, + "proof_generated": true, + "proof_mode": "core", + "proof_system": "sp1_core", + "public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#), + "prover_backend": "local_execute", + "receipt_event_id": receipt_event_id.as_str(), + "receipt_kind": KIND_TRADE_VALIDATION_RECEIPT, + "reducer_output_root": hash32('4'), + "request_event_id": root_event_id.as_str(), + "request_hash": hash32('9'), + "sp1_execute_checked": true, + "sp1_execute_public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#), + "status": "succeeded", + "validation_authority": "trusted_service_and_proof_verified", + "worker_pubkey": public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX), + "worker_role": "non_authoritative_prover" + }) + .to_string(); + signed_raw_event( + SERVICE_SECRET_KEY_HEX, + created_at, + WireEventParts { + kind: KIND_TRADE_TRANSITION_PROOF_RESULT, + content, + tags: vec![ + vec!["e".to_owned(), root_event_id.as_str().to_owned()], + vec![ + "radroots:validation_receipt".to_owned(), + receipt_event_id.as_str().to_owned(), + ], + ], + }, + ) +} + fn signed_raw_worker_result_event( raw_order_id: &str, receipt_event_id: &RadrootsEventId, @@ -5769,7 +6036,7 @@ async fn order_status_contract_dtos_serialize_deterministically() { let (_tempdir, sdk, _store) = directory_sdk_and_store().await; let request = status_request("order-1").with_limit(25); let request_json = serde_json::to_value(&request).expect("request json"); - assert_struct_serialize_error_paths(&request, 3); + assert_struct_serialize_error_paths(&request, 4); assert_eq!( request_json, @@ -5782,7 +6049,12 @@ async fn order_status_contract_dtos_serialize_deterministically() { "seller_pubkey": null }, "limit": 25, - "source": "local_only" + "source": "local_only", + "validation_trust_policy": { + "trusted_rhi_pubkeys": [], + "allow_deterministic_none": false, + "require_cryptographic_proof": true + } }) ); @@ -5803,11 +6075,13 @@ async fn order_status_contract_dtos_serialize_deterministically() { serde_json::Value::Null ); assert_eq!(receipt_json["economics"], serde_json::Value::Null); + assert_eq!(receipt_json["validation_trust"], serde_json::Value::Null); assert_eq!(receipt_json["online_evidence"], serde_json::Value::Null); assert_eq!(receipt_json["next_action"], "no_local_order"); assert_eq!(receipt_json["evidence"]["event_count"], 0); assert_eq!(receipt_json["evidence"]["limit_applied"], 25); assert_eq!(receipt_json["evidence"]["has_request"], false); + assert_eq!(receipt_json["evidence"]["has_validation_receipt"], false); assert_eq!(receipt_json["eligibility"]["can_decide"], false); assert_eq!(receipt_json["eligibility"]["can_cancel"], false); assert_eq!(receipt_json["eligibility"]["can_propose_revision"], false); diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -154,6 +154,7 @@ const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[ "TradeValidationReceiptRelayTransportOutcomeKind", "TradeValidationReceiptTags", "TradeValidationReceiptVerifyRequest", + "TradeValidationTrustDecision", "TradeValidationReceiptWorkerEvidence", "TradeValidationReceiptWorkerEvidenceSelection", "SdkTradeStatusIssue", @@ -246,6 +247,8 @@ const REQUIRED_TRADE_POLICY_EXPORTS: &[&str] = &[ "PrivacyPreflightReceipt", "PrivacyPreflightStatus", "ProductSensitivityField", + "RadrootsTradeValidationTrustPolicy", + "RadrootsTradeValidationTrustState", "SDK_TRADE_PROJECTION_CACHE_VERSION", "SdkTradeProjectionCache", "SdkTradeProjectionCacheKey",