lib

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

commit 2d419433ca8e2ed56aa5539eb4b7d5d2d3a80a2c
parent 4f65d0159bf31dc048110b46c20a54b9d371c9aa
Author: triesap <tyson@radroots.org>
Date:   Sat,  4 Jul 2026 01:48:04 +0000

trade: add validation trust policy

- add production-default trade validation trust policy
- add stable validation trust state labels
- require explicit dev/test deterministic-none policy
- cover empty production policy and label contracts

Diffstat:
Mcrates/trade/src/validation_receipt.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 145 insertions(+), 2 deletions(-)

diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -8,7 +8,9 @@ use alloc::{ }; use base64::Engine as _; -use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D}; +use radroots_events::{ + RadrootsNostrEvent, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D, +}; use radroots_events_codec::wire::WireEventParts; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -130,6 +132,96 @@ impl RadrootsTradeCommitmentConfidence { } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsTradeValidationTrustPolicy { + pub trusted_rhi_pubkeys: Vec<RadrootsPublicKey>, + pub allow_deterministic_none: bool, + pub require_cryptographic_proof: bool, +} + +impl Default for RadrootsTradeValidationTrustPolicy { + fn default() -> Self { + Self::production() + } +} + +impl RadrootsTradeValidationTrustPolicy { + pub fn production() -> Self { + Self { + trusted_rhi_pubkeys: Vec::new(), + allow_deterministic_none: false, + require_cryptographic_proof: true, + } + } + + pub fn explicit_dev_test() -> Self { + Self { + trusted_rhi_pubkeys: Vec::new(), + allow_deterministic_none: true, + require_cryptographic_proof: false, + } + } + + pub fn with_trusted_rhi_pubkeys(mut self, pubkeys: Vec<RadrootsPublicKey>) -> Self { + self.trusted_rhi_pubkeys = pubkeys; + self + } + + pub fn with_allow_deterministic_none(mut self, allow_deterministic_none: bool) -> Self { + self.allow_deterministic_none = allow_deterministic_none; + self + } + + pub fn with_require_cryptographic_proof(mut self, require_cryptographic_proof: bool) -> Self { + self.require_cryptographic_proof = require_cryptographic_proof; + self + } + + pub fn trusts_rhi_pubkey(&self, pubkey: &RadrootsPublicKey) -> bool { + self.trusted_rhi_pubkeys + .iter() + .any(|trusted| trusted == pubkey) + } + + pub fn trusted_rhi_pubkey_count(&self) -> usize { + self.trusted_rhi_pubkeys.len() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsTradeValidationTrustState { + Pending, + Untrusted, + TrustedLocal, + CryptographicCommitted, + Invalid, +} + +impl RadrootsTradeValidationTrustState { + pub const fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Untrusted => "untrusted", + Self::TrustedLocal => "trusted_local", + Self::CryptographicCommitted => "cryptographic_committed", + Self::Invalid => "invalid", + } + } + + pub fn from_label(value: &str) -> Option<Self> { + match value { + "pending" => Some(Self::Pending), + "untrusted" => Some(Self::Untrusted), + "trusted_local" => Some(Self::TrustedLocal), + "cryptographic_committed" => Some(Self::CryptographicCommitted), + "invalid" => Some(Self::Invalid), + _ => None, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RadrootsValidationReceiptProofSystem { @@ -764,7 +856,8 @@ fn zero_error_bitmap() -> &'static str { mod tests { use super::{ RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority, - RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, + RadrootsTradeValidationReceipt, RadrootsTradeValidationTrustPolicy, + RadrootsTradeValidationTrustState, RadrootsValidationReceiptError, RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, @@ -1565,6 +1658,56 @@ mod tests { } #[test] + fn validation_trust_state_contract_uses_stable_snake_case_labels() { + for (state, label) in [ + (RadrootsTradeValidationTrustState::Pending, "pending"), + (RadrootsTradeValidationTrustState::Untrusted, "untrusted"), + ( + RadrootsTradeValidationTrustState::TrustedLocal, + "trusted_local", + ), + ( + RadrootsTradeValidationTrustState::CryptographicCommitted, + "cryptographic_committed", + ), + (RadrootsTradeValidationTrustState::Invalid, "invalid"), + ] { + assert_eq!(state.as_str(), label); + assert_eq!( + RadrootsTradeValidationTrustState::from_label(label), + Some(state) + ); + assert_eq!( + serde_json::to_string(&state).expect("serialize trust state"), + format!("\"{label}\"") + ); + assert_eq!( + serde_json::from_str::<RadrootsTradeValidationTrustState>(&format!("\"{label}\"")) + .expect("deserialize trust state"), + state + ); + } + assert_eq!( + RadrootsTradeValidationTrustState::from_label("legacy"), + None + ); + } + + #[test] + fn validation_trust_policy_defaults_to_empty_production_trust() { + let production = RadrootsTradeValidationTrustPolicy::default(); + assert!(production.trusted_rhi_pubkeys.is_empty()); + assert!(!production.allow_deterministic_none); + assert!(production.require_cryptographic_proof); + assert_eq!(production.trusted_rhi_pubkey_count(), 0); + + let dev_test = RadrootsTradeValidationTrustPolicy::explicit_dev_test(); + assert!(dev_test.trusted_rhi_pubkeys.is_empty()); + assert!(dev_test.allow_deterministic_none); + assert!(!dev_test.require_cryptographic_proof); + } + + #[test] fn validation_receipt_public_values_hash_uses_domain_separator() { assert_ne!( validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),