lib

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

commit b56c5bd6caeee53c4cd9f5e99099e4215eee6294
parent de4fccb0203c665e2f2caf5e3c852956c5ecfa15
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 17:34:27 +0000

trade: add validator-set validation receipts

Introduce ValidatorSetV1 kind 30381 and bind trade validation receipts to immutable validator-set address and event identity.\n\nUpdate trade projection/workflow confidence states and SP1 guest/host public values so deterministic validator-set receipts and optional fail-closed SP1 evidence share the same receipt contract.

Diffstat:
Mcrates/event/src/kinds.rs | 1+
Mcrates/trade/src/projection.rs | 8++++++++
Mcrates/trade/src/validation_receipt.rs | 636+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/trade/src/workflow.rs | 9++++++++-
Mcrates/trade_sp1_guest/src/lib.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade_sp1_host/src/lib.rs | 46++++++++++++++++++++++++++++++++++++++--------
6 files changed, 645 insertions(+), 104 deletions(-)

diff --git a/crates/event/src/kinds.rs b/crates/event/src/kinds.rs @@ -73,6 +73,7 @@ pub const KIND_DOCUMENT: u32 = 30361; pub const KIND_RESOURCE_AREA: u32 = 30370; pub const KIND_RESOURCE_HARVEST_CAP: u32 = 30371; pub const KIND_ACCOUNT_CLAIM: u32 = 30380; +pub const KIND_VALIDATOR_SET: u32 = 30381; pub const KIND_APP_DATA: u32 = 30078; pub const KIND_FARM_WORKSPACE_MANIFEST: u32 = KIND_APP_DATA; pub const KIND_LISTING: u32 = 30402; diff --git a/crates/trade/src/projection.rs b/crates/trade/src/projection.rs @@ -947,6 +947,7 @@ mod tests { RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, validation_receipt_event_build, validation_receipt_public_values_hash_hex, + validator_set_address_from_str, }; const SELLER_SECRET: &str = "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; @@ -1171,6 +1172,13 @@ mod tests { listing_event_id: listing_event_id.into_string(), root_event_id: request_id.into_string(), target_event_id: decision_id.into_string(), + validator_set_addr: validator_set_address_from_str( + "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde", + ) + .expect("validator set address"), + validator_set_event_id: + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + .to_owned(), statement_type: RadrootsValidationReceiptType::TradeTransition, }, version: 1, diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -9,8 +9,11 @@ use alloc::{ use base64::Engine as _; use radroots_event::{ - RadrootsEventEnvelope, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT, - tags::TAG_D, wire::RadrootsNip01EventWireParts, + RadrootsEventEnvelope, + ids::{RadrootsAddressableCoordinate, RadrootsAddressableCoordinateParts, RadrootsPublicKey}, + kinds::{KIND_TRADE_VALIDATION_RECEIPT, KIND_VALIDATOR_SET}, + tags::{TAG_A, TAG_D}, + wire::RadrootsNip01EventWireParts, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -26,6 +29,31 @@ pub const TAG_VALIDATION_RECEIPT_PROOF_SYSTEM: &str = "proof_system"; pub const TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH: &str = "public_values_hash"; pub const TAG_VALIDATION_RECEIPT_RECEIPT_TYPE: &str = "receipt_type"; pub const TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT: &str = "reducer_output_root"; +pub const TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER: &str = "validator_set"; +pub const VALIDATOR_SET_V1_OPERATOR_CONTACT_MAX_CHARS: usize = 240; +pub const VALIDATOR_SET_V1_OPERATOR_NAME_MAX_CHARS: usize = 120; +pub const VALIDATOR_SET_V1_THRESHOLD: u8 = 1; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsValidatorSetV1 { + pub set_id: String, + pub validator_pubkey: RadrootsPublicKey, + pub threshold: u8, + pub valid_from: u64, + pub valid_until: u64, + pub protocol_contract_hash: String, + pub operator_name: String, + pub operator_contact: Option<String>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsVerifiedValidatorSetV1 { + pub set: RadrootsValidatorSetV1, + pub event_id: String, + pub address: RadrootsAddressableCoordinate, + pub authority_pubkey: RadrootsPublicKey, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -67,28 +95,25 @@ pub enum RadrootsValidationReceiptResult { #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RadrootsTradeValidationAuthority { - DevDeterministicOnly, - TrustedRhiServiceKey, + ValidatorSetDeterministic, CryptographicProofVerified, - TrustedServiceAndProofVerified, + ValidatorSetAndProofVerified, } impl RadrootsTradeValidationAuthority { pub const fn as_str(self) -> &'static str { match self { - Self::DevDeterministicOnly => "dev_deterministic_only", - Self::TrustedRhiServiceKey => "trusted_rhi_service_key", + Self::ValidatorSetDeterministic => "validator_set_deterministic", Self::CryptographicProofVerified => "cryptographic_proof_verified", - Self::TrustedServiceAndProofVerified => "trusted_service_and_proof_verified", + Self::ValidatorSetAndProofVerified => "validator_set_and_proof_verified", } } pub fn from_label(value: &str) -> Option<Self> { match value { - "dev_deterministic_only" => Some(Self::DevDeterministicOnly), - "trusted_rhi_service_key" => Some(Self::TrustedRhiServiceKey), + "validator_set_deterministic" => Some(Self::ValidatorSetDeterministic), "cryptographic_proof_verified" => Some(Self::CryptographicProofVerified), - "trusted_service_and_proof_verified" => Some(Self::TrustedServiceAndProofVerified), + "validator_set_and_proof_verified" => Some(Self::ValidatorSetAndProofVerified), _ => None, } } @@ -97,35 +122,30 @@ impl RadrootsTradeValidationAuthority { #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RadrootsTradeCommitmentConfidence { - LocalOnly, AwaitingValidation, - CommittedByTrustedService, + CommittedByValidatorSet, CommittedByCryptographicProof, - CommittedByTrustedServiceAndProof, + CommittedByValidatorSetAndProof, Invalid, } impl RadrootsTradeCommitmentConfidence { pub const fn as_str(self) -> &'static str { match self { - Self::LocalOnly => "local_only", Self::AwaitingValidation => "awaiting_validation", - Self::CommittedByTrustedService => "committed_by_trusted_service", + Self::CommittedByValidatorSet => "committed_by_validator_set", Self::CommittedByCryptographicProof => "committed_by_cryptographic_proof", - Self::CommittedByTrustedServiceAndProof => "committed_by_trusted_service_and_proof", + Self::CommittedByValidatorSetAndProof => "committed_by_validator_set_and_proof", Self::Invalid => "invalid", } } pub fn from_label(value: &str) -> Option<Self> { match value { - "local_only" => Some(Self::LocalOnly), "awaiting_validation" => Some(Self::AwaitingValidation), - "committed_by_trusted_service" => Some(Self::CommittedByTrustedService), + "committed_by_validator_set" => Some(Self::CommittedByValidatorSet), "committed_by_cryptographic_proof" => Some(Self::CommittedByCryptographicProof), - "committed_by_trusted_service_and_proof" => { - Some(Self::CommittedByTrustedServiceAndProof) - } + "committed_by_validator_set_and_proof" => Some(Self::CommittedByValidatorSetAndProof), "invalid" => Some(Self::Invalid), _ => None, } @@ -135,8 +155,9 @@ 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 validator_set: Option<RadrootsValidatorSetV1>, + pub validator_set_addr: Option<RadrootsAddressableCoordinate>, + pub validator_set_event_id: Option<String>, pub require_cryptographic_proof: bool, } @@ -149,28 +170,33 @@ impl Default for RadrootsTradeValidationTrustPolicy { impl RadrootsTradeValidationTrustPolicy { pub fn production() -> Self { Self { - trusted_rhi_pubkeys: Vec::new(), - allow_deterministic_none: false, - require_cryptographic_proof: true, + validator_set: None, + validator_set_addr: None, + validator_set_event_id: None, + require_cryptographic_proof: false, } } pub fn explicit_dev_test() -> Self { - Self { - trusted_rhi_pubkeys: Vec::new(), - allow_deterministic_none: true, - require_cryptographic_proof: false, - } + Self::production() } - pub fn with_trusted_rhi_pubkeys(mut self, pubkeys: Vec<RadrootsPublicKey>) -> Self { - self.trusted_rhi_pubkeys = pubkeys; + pub fn with_validator_set( + mut self, + validator_set: RadrootsValidatorSetV1, + validator_set_addr: RadrootsAddressableCoordinate, + validator_set_event_id: impl Into<String>, + ) -> Self { + self.validator_set = Some(validator_set); + self.validator_set_addr = Some(validator_set_addr); + self.validator_set_event_id = Some(validator_set_event_id.into()); self } - pub fn with_allow_deterministic_none(mut self, allow_deterministic_none: bool) -> Self { - self.allow_deterministic_none = allow_deterministic_none; - self + pub fn has_validator_set(&self) -> bool { + self.validator_set.is_some() + && self.validator_set_addr.is_some() + && self.validator_set_event_id.is_some() } pub fn with_require_cryptographic_proof(mut self, require_cryptographic_proof: bool) -> Self { @@ -178,14 +204,14 @@ impl RadrootsTradeValidationTrustPolicy { self } - pub fn trusts_rhi_pubkey(&self, pubkey: &RadrootsPublicKey) -> bool { - self.trusted_rhi_pubkeys - .iter() - .any(|trusted| trusted == pubkey) + pub fn trusts_validator_pubkey(&self, pubkey: &RadrootsPublicKey) -> bool { + self.validator_set + .as_ref() + .is_some_and(|validator_set| validator_set.validator_pubkey == *pubkey) } - pub fn trusted_rhi_pubkey_count(&self) -> usize { - self.trusted_rhi_pubkeys.len() + pub fn validator_count(&self) -> usize { + usize::from(self.validator_set.is_some()) } } @@ -194,7 +220,7 @@ impl RadrootsTradeValidationTrustPolicy { pub enum RadrootsTradeValidationTrustState { Pending, Untrusted, - TrustedLocal, + ValidatorSetCommitted, CryptographicCommitted, Invalid, } @@ -204,7 +230,7 @@ impl RadrootsTradeValidationTrustState { match self { Self::Pending => "pending", Self::Untrusted => "untrusted", - Self::TrustedLocal => "trusted_local", + Self::ValidatorSetCommitted => "validator_set_committed", Self::CryptographicCommitted => "cryptographic_committed", Self::Invalid => "invalid", } @@ -214,7 +240,7 @@ impl RadrootsTradeValidationTrustState { match value { "pending" => Some(Self::Pending), "untrusted" => Some(Self::Untrusted), - "trusted_local" => Some(Self::TrustedLocal), + "validator_set_committed" => Some(Self::ValidatorSetCommitted), "cryptographic_committed" => Some(Self::CryptographicCommitted), "invalid" => Some(Self::Invalid), _ => None, @@ -271,6 +297,8 @@ pub struct RadrootsValidationReceiptStatement { pub listing_event_id: String, pub root_event_id: String, pub target_event_id: String, + pub validator_set_addr: RadrootsAddressableCoordinate, + pub validator_set_event_id: String, #[serde(rename = "type")] pub statement_type: RadrootsValidationReceiptType, } @@ -314,6 +342,8 @@ pub struct RadrootsValidationReceiptTags { pub reducer_output_root: String, pub root_event_id: String, pub target_event_id: String, + pub validator_set_addr: RadrootsAddressableCoordinate, + pub validator_set_event_id: String, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -327,6 +357,8 @@ pub struct RadrootsValidationReceiptExpectedBinding<'a> { pub reducer_output_root: Option<&'a str>, pub root_event_id: Option<&'a str>, pub target_event_id: Option<&'a str>, + pub validator_set_addr: Option<&'a str>, + pub validator_set_event_id: Option<&'a str>, pub verifying_key_hash: Option<&'a str>, } @@ -360,6 +392,136 @@ pub enum RadrootsValidationReceiptError { ExpectedBindingMismatch(&'static str), } +impl RadrootsValidatorSetV1 { + pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> { + validate_uuidv7(&self.set_id, "validator_set.set_id")?; + if self.threshold != VALIDATOR_SET_V1_THRESHOLD { + return Err(RadrootsValidationReceiptError::InvalidField( + "validator_set.threshold", + )); + } + if self.valid_from >= self.valid_until { + return Err(RadrootsValidationReceiptError::InvalidField( + "validator_set.valid_until", + )); + } + validate_hash32( + &self.protocol_contract_hash, + "validator_set.protocol_contract_hash", + )?; + validate_bounded_text( + &self.operator_name, + VALIDATOR_SET_V1_OPERATOR_NAME_MAX_CHARS, + "validator_set.operator_name", + )?; + if let Some(operator_contact) = self.operator_contact.as_ref() { + validate_bounded_text( + operator_contact, + VALIDATOR_SET_V1_OPERATOR_CONTACT_MAX_CHARS, + "validator_set.operator_contact", + )?; + } + Ok(()) + } +} + +pub fn validator_set_address( + authority_pubkey: &RadrootsPublicKey, + set_id: &str, +) -> Result<RadrootsAddressableCoordinate, RadrootsValidationReceiptError> { + validate_uuidv7(set_id, "validator_set.set_id")?; + RadrootsAddressableCoordinate::parse(format!( + "{KIND_VALIDATOR_SET}:{authority_pubkey}:{set_id}" + )) + .map_err(|_| RadrootsValidationReceiptError::InvalidField("validator_set.address")) +} + +pub fn validator_set_address_from_str( + value: impl AsRef<str>, +) -> Result<RadrootsAddressableCoordinate, RadrootsValidationReceiptError> { + let address = RadrootsAddressableCoordinate::parse(value.as_ref()) + .map_err(|_| RadrootsValidationReceiptError::InvalidField("validator_set.address"))?; + validate_validator_set_address(&address, "validator_set.address")?; + Ok(address) +} + +pub fn validator_set_canonical_content( + validator_set: &RadrootsValidatorSetV1, +) -> Result<String, RadrootsValidationReceiptError> { + validator_set.validate()?; + serde_json::to_string(validator_set).map_err(|_| RadrootsValidationReceiptError::InvalidJson) +} + +pub fn validator_set_content_from_str( + content: &str, +) -> Result<RadrootsValidatorSetV1, RadrootsValidationReceiptError> { + let validator_set: RadrootsValidatorSetV1 = + serde_json::from_str(content).map_err(|_| RadrootsValidationReceiptError::InvalidJson)?; + validator_set.validate()?; + let canonical = validator_set_canonical_content(&validator_set)?; + if canonical != content { + return Err(RadrootsValidationReceiptError::NonCanonicalJson); + } + Ok(validator_set) +} + +pub fn validator_set_event_build( + validator_set: &RadrootsValidatorSetV1, +) -> Result<RadrootsNip01EventWireParts, RadrootsValidationReceiptError> { + Ok(RadrootsNip01EventWireParts { + kind: KIND_VALIDATOR_SET, + content: validator_set_canonical_content(validator_set)?, + tags: vec![vec![TAG_D.to_string(), validator_set.set_id.clone()]], + }) +} + +pub fn validator_set_from_event( + event: &RadrootsEventEnvelope, +) -> Result<RadrootsVerifiedValidatorSetV1, RadrootsValidationReceiptError> { + verify_validator_set_event(event, None) +} + +pub fn verify_validator_set_event( + event: &RadrootsEventEnvelope, + expected_author: Option<&RadrootsPublicKey>, +) -> Result<RadrootsVerifiedValidatorSetV1, RadrootsValidationReceiptError> { + if event.kind_u32() != KIND_VALIDATOR_SET { + return Err(RadrootsValidationReceiptError::InvalidKind { + expected: KIND_VALIDATOR_SET, + got: event.kind_u32(), + }); + } + if let Some(expected_author) = expected_author + && event.author() != expected_author + { + return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set.author", + )); + } + let validator_set = validator_set_content_from_str(event.content())?; + let tags = event.tags_as_vec(); + let d_tag = required_tag_value(&tags, TAG_D)?; + if d_tag != validator_set.set_id { + return Err(RadrootsValidationReceiptError::TagMismatch( + "validator_set.set_id", + )); + } + let address = validator_set_address(event.author(), &validator_set.set_id)?; + let parts = RadrootsAddressableCoordinateParts::parse(address.as_str()) + .map_err(|_| RadrootsValidationReceiptError::InvalidField("validator_set.address"))?; + if parts.kind != KIND_VALIDATOR_SET || parts.pubkey != *event.author() { + return Err(RadrootsValidationReceiptError::InvalidField( + "validator_set.address", + )); + } + Ok(RadrootsVerifiedValidatorSetV1 { + set: validator_set, + event_id: event.id_str().to_owned(), + address, + authority_pubkey: event.author().clone(), + }) +} + impl RadrootsTradeValidationReceipt { pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> { if self.version != VALIDATION_RECEIPT_VERSION { @@ -385,6 +547,14 @@ impl RadrootsTradeValidationReceipt { )?; validate_event_id(&self.statement.root_event_id, "statement.root_event_id")?; validate_event_id(&self.statement.target_event_id, "statement.target_event_id")?; + validate_event_id( + &self.statement.validator_set_event_id, + "statement.validator_set_event_id", + )?; + validate_validator_set_address( + &self.statement.validator_set_addr, + "statement.validator_set_addr", + )?; validate_result_error_bitmap(self.result, &self.error_bitmap)?; self.proof.validate()?; Ok(()) @@ -494,6 +664,19 @@ pub fn validation_receipt_tags( "target".to_string(), ], vec![ + TAG_A.to_string(), + receipt.statement.validator_set_addr.as_str().to_owned(), + String::new(), + TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER.to_string(), + ], + vec![ + "e".to_string(), + receipt.statement.validator_set_event_id.clone(), + String::new(), + String::new(), + TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER.to_string(), + ], + vec![ TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT.to_string(), receipt.event_set_root.clone(), ], @@ -523,6 +706,10 @@ pub fn validation_receipt_tags_from_tags( let listing_event_id = required_event_marker(tags, "listing")?; let root_event_id = required_event_marker(tags, "root")?; let target_event_id = required_event_marker(tags, "target")?; + let validator_set_addr = + required_address_marker(tags, TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER)?; + let validator_set_event_id = + required_event_marker(tags, TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER)?; let event_set_root = required_tag_value(tags, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; let reducer_output_root = required_tag_value(tags, TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT)?; let public_values_hash = required_tag_value(tags, TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH)?; @@ -539,6 +726,8 @@ pub fn validation_receipt_tags_from_tags( validate_event_id(&listing_event_id, "tags.e.listing")?; validate_event_id(&root_event_id, "tags.e.root")?; validate_event_id(&target_event_id, "tags.e.target")?; + validate_event_id(&validator_set_event_id, "tags.e.validator_set")?; + validate_validator_set_address(&validator_set_addr, "tags.a.validator_set")?; validate_hash32(&event_set_root, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; validate_hash32( &reducer_output_root, @@ -559,6 +748,8 @@ pub fn validation_receipt_tags_from_tags( reducer_output_root, root_event_id, target_event_id, + validator_set_addr, + validator_set_event_id, }) } @@ -607,6 +798,16 @@ pub fn verify_validation_receipt_event( "target_event_id", )); } + if tags.validator_set_addr != receipt.statement.validator_set_addr { + return Err(RadrootsValidationReceiptError::TagMismatch( + "validator_set_addr", + )); + } + if tags.validator_set_event_id != receipt.statement.validator_set_event_id { + return Err(RadrootsValidationReceiptError::TagMismatch( + "validator_set_event_id", + )); + } if tags.event_set_root != receipt.event_set_root { return Err(RadrootsValidationReceiptError::TagMismatch( "event_set_root", @@ -667,6 +868,20 @@ fn validate_expected_binding( "target_event_id", )); } + if let Some(validator_set_addr) = expected.validator_set_addr + && tags.validator_set_addr.as_str() != validator_set_addr + { + return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set_addr", + )); + } + if let Some(validator_set_event_id) = expected.validator_set_event_id + && tags.validator_set_event_id != validator_set_event_id + { + return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set_event_id", + )); + } if let Some(event_set_root) = expected.event_set_root && tags.event_set_root != event_set_root { @@ -751,6 +966,28 @@ fn required_event_marker( Ok(value.clone()) } +fn required_address_marker( + tags: &[Vec<String>], + marker: &'static str, +) -> Result<RadrootsAddressableCoordinate, RadrootsValidationReceiptError> { + let mut matches = tags.iter().filter(|tag| { + tag.first().map(|value| value.as_str()) == Some(TAG_A) + && tag.get(3).map(|value| value.as_str()) == Some(marker) + }); + let tag = matches + .next() + .ok_or(RadrootsValidationReceiptError::MissingTag(marker))?; + if matches.next().is_some() { + return Err(RadrootsValidationReceiptError::InvalidTag(marker)); + } + let value = tag + .get(1) + .ok_or(RadrootsValidationReceiptError::InvalidTag(marker))?; + validate_required_str(value, marker)?; + RadrootsAddressableCoordinate::parse(value) + .map_err(|_| RadrootsValidationReceiptError::InvalidTag(marker)) +} + fn validate_required_option_hash32( value: &Option<String>, field: &'static str, @@ -771,6 +1008,54 @@ fn validate_required_str( Ok(()) } +fn validate_bounded_text( + value: &str, + max_chars: usize, + field: &'static str, +) -> Result<(), RadrootsValidationReceiptError> { + validate_required_str(value, field)?; + if value.chars().count() > max_chars { + return Err(RadrootsValidationReceiptError::InvalidField(field)); + } + Ok(()) +} + +fn validate_uuidv7(value: &str, field: &'static str) -> Result<(), RadrootsValidationReceiptError> { + validate_required_str(value, field)?; + let bytes = value.as_bytes(); + if bytes.len() != 36 + || bytes[8] != b'-' + || bytes[13] != b'-' + || bytes[18] != b'-' + || bytes[23] != b'-' + || bytes[14] != b'7' + || !matches!(bytes[19], b'8'..=b'9' | b'a'..=b'b') + { + return Err(RadrootsValidationReceiptError::InvalidField(field)); + } + for (index, byte) in bytes.iter().enumerate() { + if matches!(index, 8 | 13 | 18 | 23) { + continue; + } + if !byte.is_ascii_digit() && !(b'a'..=b'f').contains(byte) { + return Err(RadrootsValidationReceiptError::InvalidField(field)); + } + } + Ok(()) +} + +fn validate_validator_set_address( + value: &RadrootsAddressableCoordinate, + field: &'static str, +) -> Result<(), RadrootsValidationReceiptError> { + let parts = RadrootsAddressableCoordinateParts::parse(value.as_str()) + .map_err(|_| RadrootsValidationReceiptError::InvalidField(field))?; + if parts.kind != KIND_VALIDATOR_SET { + return Err(RadrootsValidationReceiptError::InvalidField(field)); + } + validate_uuidv7(parts.d_tag.as_str(), field) +} + fn validate_inline_proof_base64(value: &str) -> Result<(), RadrootsValidationReceiptError> { validate_required_str(value, "proof.inline_proof_base64")?; base64::engine::general_purpose::STANDARD @@ -854,18 +1139,22 @@ mod tests { RadrootsTradeValidationTrustState, RadrootsValidationReceiptError, RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, - RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, + RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, RadrootsValidatorSetV1, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT, TAG_VALIDATION_RECEIPT_PROOF_SYSTEM, TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH, TAG_VALIDATION_RECEIPT_RECEIPT_TYPE, - TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT, validation_receipt_canonical_content, - validation_receipt_content_from_str, validation_receipt_event_build, - validation_receipt_from_event, validation_receipt_public_values_hash_hex, - validation_receipt_tags, validation_receipt_tags_from_tags, - verify_validation_receipt_event, + TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT, TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER, + validation_receipt_canonical_content, validation_receipt_content_from_str, + validation_receipt_event_build, validation_receipt_from_event, + validation_receipt_public_values_hash_hex, validation_receipt_tags, + validation_receipt_tags_from_tags, validator_set_address, validator_set_canonical_content, + validator_set_event_build, validator_set_from_event, verify_validation_receipt_event, + verify_validator_set_event, }; use radroots_event::{ - RadrootsEventEnvelope, RadrootsEventEnvelopeParts, ids::RadrootsPublicKey, - kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, + ids::RadrootsPublicKey, + kinds::{KIND_TRADE_VALIDATION_RECEIPT, KIND_VALIDATOR_SET}, + tags::TAG_D, }; fn hash32(c: char) -> String { @@ -876,6 +1165,36 @@ mod tests { c.to_string().repeat(64) } + fn validator_set_id() -> String { + "018f3d99-7d35-7c0c-8a0f-7f3b645abcde".to_string() + } + + fn validator_set_author() -> RadrootsPublicKey { + RadrootsPublicKey::parse(event_id('d')).expect("validator set author") + } + + fn validator_set_pubkey() -> RadrootsPublicKey { + RadrootsPublicKey::parse(event_id('e')).expect("validator pubkey") + } + + fn validator_set_addr() -> radroots_event::ids::RadrootsAddressableCoordinate { + validator_set_address(&validator_set_author(), &validator_set_id()) + .expect("validator set address") + } + + fn sample_validator_set() -> RadrootsValidatorSetV1 { + RadrootsValidatorSetV1 { + set_id: validator_set_id(), + validator_pubkey: validator_set_pubkey(), + threshold: 1, + valid_from: 1_700_000_000, + valid_until: 1_800_000_000, + protocol_contract_hash: hash32('7'), + operator_name: "Radroots validation operator".to_string(), + operator_contact: Some("validator@example.invalid".to_string()), + } + } + fn sample_validation_receipt() -> RadrootsTradeValidationReceipt { RadrootsTradeValidationReceipt { changed_records_root: hash32('6'), @@ -901,6 +1220,8 @@ mod tests { listing_event_id: event_id('0'), root_event_id: event_id('1'), target_event_id: event_id('2'), + validator_set_addr: validator_set_addr(), + validator_set_event_id: event_id('8'), statement_type: RadrootsValidationReceiptType::TradeTransition, }, version: 1, @@ -1050,20 +1371,68 @@ mod tests { #[test] fn validation_trust_policy_builders_preserve_explicit_settings() { - let trusted = RadrootsPublicKey::parse(event_id('a')).unwrap(); + let trusted = validator_set_pubkey(); let other = RadrootsPublicKey::parse(event_id('b')).unwrap(); let policy = RadrootsTradeValidationTrustPolicy::production() - .with_trusted_rhi_pubkeys(vec![trusted.clone()]) - .with_allow_deterministic_none(true) + .with_validator_set(sample_validator_set(), validator_set_addr(), event_id('8')) .with_require_cryptographic_proof(false); - assert_eq!(policy.trusted_rhi_pubkey_count(), 1); - assert!(policy.trusts_rhi_pubkey(&trusted)); - assert!(!policy.trusts_rhi_pubkey(&other)); - assert!(policy.allow_deterministic_none); + assert_eq!(policy.validator_count(), 1); + assert!(policy.has_validator_set()); + assert!(policy.trusts_validator_pubkey(&trusted)); + assert!(!policy.trusts_validator_pubkey(&other)); assert!(!policy.require_cryptographic_proof); - assert!(RadrootsTradeValidationTrustPolicy::default().require_cryptographic_proof); - assert!(RadrootsTradeValidationTrustPolicy::explicit_dev_test().allow_deterministic_none); + assert!(!RadrootsTradeValidationTrustPolicy::default().require_cryptographic_proof); + assert!( + !RadrootsTradeValidationTrustPolicy::explicit_dev_test().require_cryptographic_proof + ); + } + + #[test] + fn validator_set_round_trips_canonical_payload_and_address() { + let validator_set = sample_validator_set(); + let content = + validator_set_canonical_content(&validator_set).expect("validator set content"); + assert_eq!( + content, + format!( + "{{\"set_id\":\"{}\",\"validator_pubkey\":\"{}\",\"threshold\":1,\"valid_from\":1700000000,\"valid_until\":1800000000,\"protocol_contract_hash\":\"{}\",\"operator_name\":\"Radroots validation operator\",\"operator_contact\":\"validator@example.invalid\"}}", + validator_set_id(), + validator_set_pubkey(), + hash32('7'), + ) + ); + let parts = validator_set_event_build(&validator_set).expect("validator set parts"); + assert_eq!(parts.kind, KIND_VALIDATOR_SET); + assert_eq!( + parts.tags, + vec![vec![TAG_D.to_string(), validator_set_id()]] + ); + + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: event_id('7'), + author: validator_set_author().as_str().to_string(), + created_at: 1_700_000_001, + kind: parts.kind, + tags: parts.tags, + content: parts.content, + sig: "f".repeat(128), + }) + .expect("validator set event"); + + let verified = validator_set_from_event(&event).expect("verified validator set"); + assert_eq!(verified.set, validator_set); + assert_eq!(verified.event_id, event_id('7')); + assert_eq!(verified.address, validator_set_addr()); + assert_eq!(verified.authority_pubkey, validator_set_author()); + verify_validator_set_event(&event, Some(&validator_set_author())) + .expect("expected authority"); + assert_eq!( + verify_validator_set_event(&event, Some(&validator_set_pubkey())), + Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set.author" + )) + ); } #[test] @@ -1366,8 +1735,26 @@ mod tests { )) ); + let mut invalid_validator_set_addr = tags.clone(); + invalid_validator_set_addr[4][1] = "bad".to_string(); + assert_eq!( + validation_receipt_tags_from_tags(&invalid_validator_set_addr), + Err(RadrootsValidationReceiptError::InvalidTag( + TAG_VALIDATION_RECEIPT_VALIDATOR_SET_MARKER + )) + ); + + let mut invalid_validator_set_event = tags.clone(); + invalid_validator_set_event[5][1] = "bad".to_string(); + assert_eq!( + validation_receipt_tags_from_tags(&invalid_validator_set_event), + Err(RadrootsValidationReceiptError::InvalidField( + "tags.e.validator_set" + )) + ); + let mut invalid_event_set = tags.clone(); - invalid_event_set[4][1] = "bad".to_string(); + invalid_event_set[6][1] = "bad".to_string(); assert_eq!( validation_receipt_tags_from_tags(&invalid_event_set), Err(RadrootsValidationReceiptError::InvalidField( @@ -1376,7 +1763,7 @@ mod tests { ); let mut invalid_reducer = tags.clone(); - invalid_reducer[5][1] = "bad".to_string(); + invalid_reducer[7][1] = "bad".to_string(); assert_eq!( validation_receipt_tags_from_tags(&invalid_reducer), Err(RadrootsValidationReceiptError::InvalidField( @@ -1385,7 +1772,7 @@ mod tests { ); let mut invalid_public_values = tags.clone(); - invalid_public_values[6][1] = "bad".to_string(); + invalid_public_values[8][1] = "bad".to_string(); assert_eq!( validation_receipt_tags_from_tags(&invalid_public_values), Err(RadrootsValidationReceiptError::InvalidField( @@ -1394,7 +1781,7 @@ mod tests { ); let mut invalid_proof_system = tags.clone(); - invalid_proof_system[7][1] = "sp1_unknown".to_string(); + invalid_proof_system[9][1] = "sp1_unknown".to_string(); assert_eq!( validation_receipt_tags_from_tags(&invalid_proof_system), Err(RadrootsValidationReceiptError::InvalidTag( @@ -1403,7 +1790,7 @@ mod tests { ); let mut invalid_receipt_type = tags.clone(); - invalid_receipt_type[8][1] = "unknown".to_string(); + invalid_receipt_type[10][1] = "unknown".to_string(); assert_eq!( validation_receipt_tags_from_tags(&invalid_receipt_type), Err(RadrootsValidationReceiptError::InvalidTag( @@ -1493,7 +1880,32 @@ mod tests { ); let mut tags = sample_validation_receipt_event().tags_as_vec(); - tags[4][1] = hash32('d'); + tags[4][1] = format!( + "{}:{}:{}", + KIND_VALIDATOR_SET, + event_id('a'), + validator_set_id() + ); + let event = validation_receipt_event_with_tags(tags); + assert_eq!( + validation_receipt_from_event(&event), + Err(RadrootsValidationReceiptError::TagMismatch( + "validator_set_addr" + )) + ); + + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[5][1] = event_id('3'); + let event = validation_receipt_event_with_tags(tags); + assert_eq!( + validation_receipt_from_event(&event), + Err(RadrootsValidationReceiptError::TagMismatch( + "validator_set_event_id" + )) + ); + + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[6][1] = hash32('d'); let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), @@ -1503,7 +1915,7 @@ mod tests { ); let mut tags = sample_validation_receipt_event().tags_as_vec(); - tags[5][1] = hash32('d'); + tags[7][1] = hash32('d'); let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), @@ -1513,7 +1925,7 @@ mod tests { ); let mut tags = sample_validation_receipt_event().tags_as_vec(); - tags[6][1] = hash32('d'); + tags[8][1] = hash32('d'); let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), @@ -1523,7 +1935,7 @@ mod tests { ); let mut tags = sample_validation_receipt_event().tags_as_vec(); - tags[7][1] = "sp1_core".to_string(); + tags[9][1] = "sp1_core".to_string(); let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), @@ -1531,7 +1943,7 @@ mod tests { ); let mut tags = sample_validation_receipt_event().tags_as_vec(); - tags[8][1] = "listing_validation".to_string(); + tags[10][1] = "listing_validation".to_string(); let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), @@ -1542,6 +1954,8 @@ mod tests { #[test] fn validation_receipt_expected_binding_checks_all_supported_fields() { let event = sample_validation_receipt_event(); + let validator_set_addr = validator_set_addr(); + let validator_set_addr_raw = validator_set_addr.as_str().to_string(); verify_validation_receipt_event( &event, RadrootsValidationReceiptExpectedBinding { @@ -1555,11 +1969,19 @@ mod tests { reducer_output_root: Some(&hash32('4')), root_event_id: Some(&event_id('1')), target_event_id: Some(&event_id('2')), + validator_set_addr: Some(validator_set_addr_raw.as_str()), + validator_set_event_id: Some(&event_id('8')), ..RadrootsValidationReceiptExpectedBinding::default() }, ) .expect("matching expected binding"); + let wrong_validator_set_addr = format!( + "{}:{}:{}", + KIND_VALIDATOR_SET, + event_id('a'), + validator_set_id() + ); assert_eq!( verify_validation_receipt_event( &event, @@ -1600,6 +2022,30 @@ mod tests { verify_validation_receipt_event( &event, RadrootsValidationReceiptExpectedBinding { + validator_set_addr: Some(wrong_validator_set_addr.as_str()), + ..RadrootsValidationReceiptExpectedBinding::default() + }, + ), + Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set_addr" + )) + ); + assert_eq!( + verify_validation_receipt_event( + &event, + RadrootsValidationReceiptExpectedBinding { + validator_set_event_id: Some(&event_id('3')), + ..RadrootsValidationReceiptExpectedBinding::default() + }, + ), + Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "validator_set_event_id" + )) + ); + assert_eq!( + verify_validation_receipt_event( + &event, + RadrootsValidationReceiptExpectedBinding { event_set_root: Some(&hash32('d')), ..RadrootsValidationReceiptExpectedBinding::default() }, @@ -1665,7 +2111,7 @@ mod tests { assert_eq!( content, format!( - "{{\"changed_records_root\":\"{}\",\"domain\":\"radroots.receipt\",\"error_bitmap\":\"0x00000000000000000000000000000000\",\"event_set_root\":\"{}\",\"new_state_root\":\"{}\",\"previous_state_root\":\"{}\",\"proof\":{{\"inline_proof_base64\":null,\"mode\":null,\"program_hash\":null,\"proof_reference\":null,\"system\":\"none\",\"verifying_key_hash\":null}},\"public_values_hash\":\"{}\",\"receipt_type\":\"trade_transition\",\"result\":\"valid\",\"statement\":{{\"listing_event_id\":\"{}\",\"root_event_id\":\"{}\",\"target_event_id\":\"{}\",\"type\":\"trade_transition\"}},\"version\":1}}", + "{{\"changed_records_root\":\"{}\",\"domain\":\"radroots.receipt\",\"error_bitmap\":\"0x00000000000000000000000000000000\",\"event_set_root\":\"{}\",\"new_state_root\":\"{}\",\"previous_state_root\":\"{}\",\"proof\":{{\"inline_proof_base64\":null,\"mode\":null,\"program_hash\":null,\"proof_reference\":null,\"system\":\"none\",\"verifying_key_hash\":null}},\"public_values_hash\":\"{}\",\"receipt_type\":\"trade_transition\",\"result\":\"valid\",\"statement\":{{\"listing_event_id\":\"{}\",\"root_event_id\":\"{}\",\"target_event_id\":\"{}\",\"validator_set_addr\":\"{}\",\"validator_set_event_id\":\"{}\",\"type\":\"trade_transition\"}},\"version\":1}}", hash32('6'), hash32('c'), hash32('4'), @@ -1674,6 +2120,8 @@ mod tests { event_id('0'), event_id('1'), event_id('2'), + validator_set_addr().as_str(), + event_id('8'), ) ); assert_eq!( @@ -1686,6 +2134,8 @@ mod tests { let verified = validation_receipt_from_event(&event).expect("verified receipt"); assert_eq!(verified.tags.order_id, "order-1"); assert_eq!(verified.tags.listing_event_id, event_id('0')); + assert_eq!(verified.tags.validator_set_addr, validator_set_addr()); + assert_eq!(verified.tags.validator_set_event_id, event_id('8')); assert_eq!(verified.tags.event_set_root, hash32('c')); assert_eq!(verified.tags.reducer_output_root, hash32('4')); assert_eq!( @@ -1698,20 +2148,16 @@ mod tests { fn validation_authority_contract_uses_stable_snake_case_labels() { for (authority, label) in [ ( - RadrootsTradeValidationAuthority::DevDeterministicOnly, - "dev_deterministic_only", - ), - ( - RadrootsTradeValidationAuthority::TrustedRhiServiceKey, - "trusted_rhi_service_key", + RadrootsTradeValidationAuthority::ValidatorSetDeterministic, + "validator_set_deterministic", ), ( RadrootsTradeValidationAuthority::CryptographicProofVerified, "cryptographic_proof_verified", ), ( - RadrootsTradeValidationAuthority::TrustedServiceAndProofVerified, - "trusted_service_and_proof_verified", + RadrootsTradeValidationAuthority::ValidatorSetAndProofVerified, + "validator_set_and_proof_verified", ), ] { assert_eq!(authority.as_str(), label); @@ -1735,22 +2181,21 @@ mod tests { #[test] fn commitment_confidence_contract_uses_stable_snake_case_labels() { for (confidence, label) in [ - (RadrootsTradeCommitmentConfidence::LocalOnly, "local_only"), ( RadrootsTradeCommitmentConfidence::AwaitingValidation, "awaiting_validation", ), ( - RadrootsTradeCommitmentConfidence::CommittedByTrustedService, - "committed_by_trusted_service", + RadrootsTradeCommitmentConfidence::CommittedByValidatorSet, + "committed_by_validator_set", ), ( RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof, "committed_by_cryptographic_proof", ), ( - RadrootsTradeCommitmentConfidence::CommittedByTrustedServiceAndProof, - "committed_by_trusted_service_and_proof", + RadrootsTradeCommitmentConfidence::CommittedByValidatorSetAndProof, + "committed_by_validator_set_and_proof", ), (RadrootsTradeCommitmentConfidence::Invalid, "invalid"), ] { @@ -1781,8 +2226,8 @@ mod tests { (RadrootsTradeValidationTrustState::Pending, "pending"), (RadrootsTradeValidationTrustState::Untrusted, "untrusted"), ( - RadrootsTradeValidationTrustState::TrustedLocal, - "trusted_local", + RadrootsTradeValidationTrustState::ValidatorSetCommitted, + "validator_set_committed", ), ( RadrootsTradeValidationTrustState::CryptographicCommitted, @@ -1814,14 +2259,15 @@ mod tests { #[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); + assert!(production.validator_set.is_none()); + assert!(production.validator_set_addr.is_none()); + assert!(production.validator_set_event_id.is_none()); + assert!(!production.has_validator_set()); + assert!(!production.require_cryptographic_proof); + assert_eq!(production.validator_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.has_validator_set()); assert!(!dev_test.require_cryptographic_proof); } diff --git a/crates/trade/src/workflow.rs b/crates/trade/src/workflow.rs @@ -330,7 +330,7 @@ mod tests { RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, validation_receipt_public_values_hash_hex, validation_receipt_tags, - validation_receipt_tags_from_tags, + validation_receipt_tags_from_tags, validator_set_address_from_str, }; use super::{ @@ -540,6 +540,13 @@ mod tests { listing_event_id: listing_event_id.into_string(), root_event_id: root_event_id.into_string(), target_event_id: target_event_id.into_string(), + validator_set_addr: validator_set_address_from_str( + "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde", + ) + .expect("validator set address"), + validator_set_event_id: + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + .to_string(), statement_type: RadrootsValidationReceiptType::TradeTransition, }, version: 1, diff --git a/crates/trade_sp1_guest/src/lib.rs b/crates/trade_sp1_guest/src/lib.rs @@ -52,6 +52,8 @@ pub struct RadrootsSp1TradeProofPublicValues { pub order_id_hash: Option<String>, pub root_event_id: Option<String>, pub target_event_id: Option<String>, + pub validator_set_addr: Option<String>, + pub validator_set_event_id: Option<String>, pub previous_state_root: String, pub new_state_root: String, pub transition: Option<RadrootsSp1TradeProofTransitionKind>, @@ -155,6 +157,8 @@ pub struct RadrootsSp1TradeOrderAcceptanceWitness { pub listing_event_id: String, pub request_event_id: String, pub decision_event_id: String, + pub validator_set_addr: String, + pub validator_set_event_id: String, pub event_evidence: Vec<RadrootsSp1TradeCanonicalEventEvidence>, pub request: RadrootsSp1TradeOrderRequestWitness, pub decision: RadrootsSp1TradeOrderDecisionEventWitness, @@ -291,6 +295,8 @@ pub fn reduce_order_acceptance_public_values( )), root_event_id: Some(witness.request_event_id.clone()), target_event_id: Some(witness.decision_event_id.clone()), + validator_set_addr: Some(witness.validator_set_addr.clone()), + validator_set_event_id: Some(witness.validator_set_event_id.clone()), previous_state_root, new_state_root, transition: Some(RadrootsSp1TradeProofTransitionKind::OrderAccepted), @@ -355,6 +361,8 @@ fn validate_witness_header( validate_event_id(&witness.listing_event_id, "listing_event_id")?; validate_event_id(&witness.request_event_id, "request_event_id")?; validate_event_id(&witness.decision_event_id, "decision_event_id")?; + validate_addressable_coordinate(&witness.validator_set_addr, "validator_set_addr")?; + validate_event_id(&witness.validator_set_event_id, "validator_set_event_id")?; validate_required_str(&witness.reducer_program_hash, "reducer_program_hash")?; validate_hash32(&witness.reducer_program_hash, "reducer_program_hash")?; if witness.reducer_program_hash != RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH { @@ -673,6 +681,16 @@ fn validate_public_values( if let Some(event_id) = &public_values.target_event_id { validate_event_id(event_id, "target_event_id")?; } + let Some(validator_set_addr) = &public_values.validator_set_addr else { + return Err(RadrootsSp1TradeGuestError::EmptyField("validator_set_addr")); + }; + validate_addressable_coordinate(validator_set_addr, "validator_set_addr")?; + let Some(validator_set_event_id) = &public_values.validator_set_event_id else { + return Err(RadrootsSp1TradeGuestError::EmptyField( + "validator_set_event_id", + )); + }; + validate_event_id(validator_set_event_id, "validator_set_event_id")?; validate_hash32(&public_values.previous_state_root, "previous_state_root")?; validate_hash32(&public_values.new_state_root, "new_state_root")?; validate_hash32(&public_values.changed_records_root, "changed_records_root")?; @@ -745,6 +763,32 @@ fn validate_event_id(value: &str, field: &'static str) -> Result<(), RadrootsSp1 Ok(()) } +fn validate_addressable_coordinate( + value: &str, + field: &'static str, +) -> Result<(), RadrootsSp1TradeGuestError> { + validate_required_str(value, field)?; + let mut parts = value.split(':'); + let Some(kind) = parts.next() else { + return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field)); + }; + let Some(pubkey) = parts.next() else { + return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field)); + }; + let Some(d_tag) = parts.next() else { + return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field)); + }; + if parts.next().is_some() + || kind.parse::<u32>().is_err() + || pubkey.len() != 64 + || !is_lower_hex(pubkey) + || d_tag.trim().is_empty() + { + return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field)); + } + Ok(()) +} + fn validate_hex64(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> { if value.len() != 64 || !is_lower_hex(value) { return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field)); @@ -835,6 +879,11 @@ mod tests { .to_string(), decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" .to_string(), + validator_set_addr: + "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" + .to_string(), + validator_set_event_id: + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".to_string(), event_evidence: event_evidence(), request: request(2), decision: decision(2), diff --git a/crates/trade_sp1_host/src/lib.rs b/crates/trade_sp1_host/src/lib.rs @@ -7,6 +7,7 @@ use radroots_trade::validation_receipt::{ RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX, VALIDATION_RECEIPT_VERSION, + validator_set_address_from_str, }; use radroots_trade_sp1_guest::{ RadrootsSp1TradeGuestError, RadrootsSp1TradeOrderAcceptanceWitness, @@ -132,8 +133,6 @@ impl RadrootsSp1TradeResolvedProofArtifact { #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RadrootsSp1TradeProverBackend { - Disabled, - DeterministicNone, LocalExecute, LocalCpuProve, LocalCudaProve, @@ -143,8 +142,6 @@ pub enum RadrootsSp1TradeProverBackend { impl RadrootsSp1TradeProverBackend { pub const fn as_str(self) -> &'static str { match self { - Self::Disabled => "disabled", - Self::DeterministicNone => "deterministic_none", Self::LocalExecute => "local_execute", Self::LocalCpuProve => "local_cpu_prove", Self::LocalCudaProve => "local_cuda_prove", @@ -154,8 +151,6 @@ impl RadrootsSp1TradeProverBackend { pub fn from_label(value: &str) -> Option<Self> { match value { - "disabled" => Some(Self::Disabled), - "deterministic_none" => Some(Self::DeterministicNone), "local_execute" => Some(Self::LocalExecute), "local_cpu_prove" => Some(Self::LocalCpuProve), "local_cuda_prove" => Some(Self::LocalCudaProve), @@ -942,6 +937,20 @@ fn verify_validation_receipt_matches_public_values( "target_event_id", )); } + if public_values.validator_set_addr.as_deref() + != Some(receipt.statement.validator_set_addr.as_str()) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "validator_set_addr", + )); + } + if public_values.validator_set_event_id.as_deref() + != Some(receipt.statement.validator_set_event_id.as_str()) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "validator_set_event_id", + )); + } if !receipt_result_matches_public_values(receipt.result, public_values.result) { return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( "result", @@ -1057,6 +1066,14 @@ pub fn validation_receipt_for_order_acceptance_proof( let target_event_id = public_values.target_event_id.clone().ok_or( RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), )?; + let validator_set_addr = public_values.validator_set_addr.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("validator_set_addr"), + )?; + let validator_set_addr = validator_set_address_from_str(validator_set_addr) + .map_err(|_| RadrootsSp1TradeHostError::MissingReceiptBinding("validator_set_addr"))?; + let validator_set_event_id = public_values.validator_set_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("validator_set_event_id"), + )?; Ok(RadrootsTradeValidationReceipt { changed_records_root: public_values.changed_records_root.clone(), domain: VALIDATION_RECEIPT_DOMAIN.to_string(), @@ -1079,6 +1096,8 @@ pub fn validation_receipt_for_order_acceptance_proof( listing_event_id, root_event_id, target_event_id, + validator_set_addr, + validator_set_event_id, statement_type: RadrootsValidationReceiptType::TradeTransition, }, version: VALIDATION_RECEIPT_VERSION, @@ -1799,6 +1818,11 @@ mod tests { .to_string(), decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" .to_string(), + validator_set_addr: + "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde" + .to_string(), + validator_set_event_id: + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".to_string(), event_evidence: event_evidence(), request: request(2), decision: decision(2), @@ -2082,8 +2106,6 @@ mod tests { assert_eq!(RadrootsSp1TradeProofMode::from_label("legacy"), None); for backend in [ - super::RadrootsSp1TradeProverBackend::Disabled, - super::RadrootsSp1TradeProverBackend::DeterministicNone, super::RadrootsSp1TradeProverBackend::LocalExecute, super::RadrootsSp1TradeProverBackend::LocalCpuProve, super::RadrootsSp1TradeProverBackend::LocalCudaProve, @@ -2099,6 +2121,14 @@ mod tests { super::RadrootsSp1TradeProverBackend::from_label("legacy"), None ); + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label("disabled"), + None + ); + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label("deterministic_none"), + None + ); for engine in [ super::RadrootsSp1TradeProofEngine::Cpu,