commit 011e4137dc62321e76ad03e842c6463ede1071fa
parent b81acdfee8c5ac031641b2e0f2a6f5fd801a2d9f
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:
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",