validation_receipt.rs (62596B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 format, 6 string::{String, ToString}, 7 vec::Vec, 8 }; 9 10 use base64::Engine as _; 11 use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D}; 12 use radroots_events_codec::wire::WireEventParts; 13 use serde::{Deserialize, Serialize}; 14 use sha2::{Digest, Sha256}; 15 use thiserror::Error; 16 17 pub const VALIDATION_RECEIPT_DOMAIN: &str = "radroots.receipt"; 18 pub const VALIDATION_RECEIPT_VERSION: u32 = 1; 19 pub const VALIDATION_RECEIPT_PUBLIC_VALUES_HASH_DOMAIN: &[u8] = b"radroots:sp1-public-values:v1"; 20 pub const VALIDATION_RECEIPT_PROOF_REFERENCE_SCHEME: &str = "radroots-proof://"; 21 pub const VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX: &str = "radroots-proof://sha256/"; 22 pub const TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT: &str = "event_set_root"; 23 pub const TAG_VALIDATION_RECEIPT_PROOF_SYSTEM: &str = "proof_system"; 24 pub const TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH: &str = "public_values_hash"; 25 pub const TAG_VALIDATION_RECEIPT_RECEIPT_TYPE: &str = "receipt_type"; 26 pub const TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT: &str = "reducer_output_root"; 27 28 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 29 #[serde(rename_all = "snake_case")] 30 pub enum RadrootsValidationReceiptType { 31 ListingValidation, 32 TradeTransition, 33 InventoryState, 34 StateCheckpoint, 35 } 36 37 impl RadrootsValidationReceiptType { 38 pub const fn as_str(self) -> &'static str { 39 match self { 40 Self::ListingValidation => "listing_validation", 41 Self::TradeTransition => "trade_transition", 42 Self::InventoryState => "inventory_state", 43 Self::StateCheckpoint => "state_checkpoint", 44 } 45 } 46 47 pub fn from_label(value: &str) -> Option<Self> { 48 match value { 49 "listing_validation" => Some(Self::ListingValidation), 50 "trade_transition" => Some(Self::TradeTransition), 51 "inventory_state" => Some(Self::InventoryState), 52 "state_checkpoint" => Some(Self::StateCheckpoint), 53 _ => None, 54 } 55 } 56 } 57 58 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 59 #[serde(rename_all = "snake_case")] 60 pub enum RadrootsValidationReceiptResult { 61 Valid, 62 Invalid, 63 } 64 65 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 66 #[serde(rename_all = "snake_case")] 67 pub enum RadrootsValidationReceiptProofSystem { 68 None, 69 Sp1Core, 70 Sp1Compressed, 71 Sp1Groth16, 72 Sp1Plonk, 73 } 74 75 impl RadrootsValidationReceiptProofSystem { 76 pub const fn as_str(self) -> &'static str { 77 match self { 78 Self::None => "none", 79 Self::Sp1Core => "sp1_core", 80 Self::Sp1Compressed => "sp1_compressed", 81 Self::Sp1Groth16 => "sp1_groth16", 82 Self::Sp1Plonk => "sp1_plonk", 83 } 84 } 85 86 pub fn from_label(value: &str) -> Option<Self> { 87 match value { 88 "none" => Some(Self::None), 89 "sp1_core" => Some(Self::Sp1Core), 90 "sp1_compressed" => Some(Self::Sp1Compressed), 91 "sp1_groth16" => Some(Self::Sp1Groth16), 92 "sp1_plonk" => Some(Self::Sp1Plonk), 93 _ => None, 94 } 95 } 96 97 const fn expected_mode(self) -> Option<&'static str> { 98 match self { 99 Self::None => None, 100 Self::Sp1Core => Some("core"), 101 Self::Sp1Compressed => Some("compressed"), 102 Self::Sp1Groth16 => Some("groth16"), 103 Self::Sp1Plonk => Some("plonk"), 104 } 105 } 106 } 107 108 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 109 #[serde(deny_unknown_fields)] 110 pub struct RadrootsValidationReceiptStatement { 111 pub listing_event_id: String, 112 pub root_event_id: String, 113 pub target_event_id: String, 114 #[serde(rename = "type")] 115 pub statement_type: RadrootsValidationReceiptType, 116 } 117 118 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 119 #[serde(deny_unknown_fields)] 120 pub struct RadrootsValidationReceiptProof { 121 pub inline_proof_base64: Option<String>, 122 pub mode: Option<String>, 123 pub program_hash: Option<String>, 124 pub proof_reference: Option<String>, 125 pub system: RadrootsValidationReceiptProofSystem, 126 pub verifying_key_hash: Option<String>, 127 } 128 129 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 130 #[serde(deny_unknown_fields)] 131 pub struct RadrootsTradeValidationReceipt { 132 pub changed_records_root: String, 133 pub domain: String, 134 pub error_bitmap: String, 135 pub event_set_root: String, 136 pub new_state_root: String, 137 pub previous_state_root: String, 138 pub proof: RadrootsValidationReceiptProof, 139 pub public_values_hash: String, 140 pub receipt_type: RadrootsValidationReceiptType, 141 pub result: RadrootsValidationReceiptResult, 142 pub statement: RadrootsValidationReceiptStatement, 143 pub version: u32, 144 } 145 146 #[derive(Clone, Debug, PartialEq, Eq)] 147 pub struct RadrootsValidationReceiptTags { 148 pub event_set_root: String, 149 pub listing_event_id: String, 150 pub order_id: String, 151 pub proof_system: RadrootsValidationReceiptProofSystem, 152 pub public_values_hash: String, 153 pub receipt_type: RadrootsValidationReceiptType, 154 pub reducer_output_root: String, 155 pub root_event_id: String, 156 pub target_event_id: String, 157 } 158 159 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 160 pub struct RadrootsValidationReceiptExpectedBinding<'a> { 161 pub event_set_root: Option<&'a str>, 162 pub listing_event_id: Option<&'a str>, 163 pub order_id: Option<&'a str>, 164 pub program_hash: Option<&'a str>, 165 pub proof_system: Option<RadrootsValidationReceiptProofSystem>, 166 pub public_values_hash: Option<&'a str>, 167 pub reducer_output_root: Option<&'a str>, 168 pub verifying_key_hash: Option<&'a str>, 169 } 170 171 #[derive(Clone, Debug, PartialEq, Eq)] 172 pub struct RadrootsVerifiedValidationReceipt { 173 pub receipt: RadrootsTradeValidationReceipt, 174 pub tags: RadrootsValidationReceiptTags, 175 } 176 177 #[derive(Clone, Debug, Error, PartialEq, Eq)] 178 pub enum RadrootsValidationReceiptError { 179 #[error("{0} cannot be empty")] 180 EmptyField(&'static str), 181 #[error("invalid event kind {got}; expected {expected}")] 182 InvalidKind { expected: u32, got: u32 }, 183 #[error("invalid validation receipt json")] 184 InvalidJson, 185 #[error("validation receipt json is not canonical")] 186 NonCanonicalJson, 187 #[error("invalid validation receipt field {0}")] 188 InvalidField(&'static str), 189 #[error("invalid validation receipt proof metadata {0}")] 190 InvalidProofMetadata(&'static str), 191 #[error("missing validation receipt tag {0}")] 192 MissingTag(&'static str), 193 #[error("invalid validation receipt tag {0}")] 194 InvalidTag(&'static str), 195 #[error("validation receipt tag {0} does not match content")] 196 TagMismatch(&'static str), 197 #[error("validation receipt expected binding {0} does not match")] 198 ExpectedBindingMismatch(&'static str), 199 } 200 201 impl RadrootsTradeValidationReceipt { 202 pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> { 203 if self.version != VALIDATION_RECEIPT_VERSION { 204 return Err(RadrootsValidationReceiptError::InvalidField("version")); 205 } 206 if self.domain != VALIDATION_RECEIPT_DOMAIN { 207 return Err(RadrootsValidationReceiptError::InvalidField("domain")); 208 } 209 if self.receipt_type != self.statement.statement_type { 210 return Err(RadrootsValidationReceiptError::InvalidField( 211 "statement.type", 212 )); 213 } 214 validate_hash32(&self.changed_records_root, "changed_records_root")?; 215 validate_error_bitmap(&self.error_bitmap)?; 216 validate_hash32(&self.event_set_root, "event_set_root")?; 217 validate_hash32(&self.new_state_root, "new_state_root")?; 218 validate_hash32(&self.previous_state_root, "previous_state_root")?; 219 validate_hash32(&self.public_values_hash, "public_values_hash")?; 220 validate_event_id( 221 &self.statement.listing_event_id, 222 "statement.listing_event_id", 223 )?; 224 validate_event_id(&self.statement.root_event_id, "statement.root_event_id")?; 225 validate_event_id(&self.statement.target_event_id, "statement.target_event_id")?; 226 validate_result_error_bitmap(self.result, &self.error_bitmap)?; 227 self.proof.validate()?; 228 Ok(()) 229 } 230 } 231 232 impl RadrootsValidationReceiptProof { 233 pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> { 234 match self.system { 235 RadrootsValidationReceiptProofSystem::None => { 236 if self.inline_proof_base64.is_some() 237 || self.mode.is_some() 238 || self.program_hash.is_some() 239 || self.proof_reference.is_some() 240 || self.verifying_key_hash.is_some() 241 { 242 return Err(RadrootsValidationReceiptError::InvalidProofMetadata( 243 "proof.system", 244 )); 245 } 246 } 247 system => { 248 validate_required_option_hash32(&self.program_hash, "proof.program_hash")?; 249 validate_required_option_hash32( 250 &self.verifying_key_hash, 251 "proof.verifying_key_hash", 252 )?; 253 if self.mode.as_deref() != system.expected_mode() { 254 return Err(RadrootsValidationReceiptError::InvalidProofMetadata( 255 "proof.mode", 256 )); 257 } 258 match (&self.inline_proof_base64, &self.proof_reference) { 259 (Some(inline), None) => validate_inline_proof_base64(inline)?, 260 (None, Some(reference)) => validate_proof_reference(reference)?, 261 (None, None) => { 262 return Err(RadrootsValidationReceiptError::InvalidProofMetadata( 263 "proof.material_missing", 264 )); 265 } 266 (Some(_), Some(_)) => { 267 return Err(RadrootsValidationReceiptError::InvalidProofMetadata( 268 "proof.material_conflict", 269 )); 270 } 271 } 272 } 273 } 274 Ok(()) 275 } 276 } 277 278 pub fn validation_receipt_public_values_hash_hex(public_values: &[u8]) -> String { 279 let mut hasher = Sha256::new(); 280 hasher.update(VALIDATION_RECEIPT_PUBLIC_VALUES_HASH_DOMAIN); 281 hasher.update(public_values); 282 format!("0x{}", hex::encode(hasher.finalize())) 283 } 284 285 pub fn validation_receipt_canonical_content( 286 receipt: &RadrootsTradeValidationReceipt, 287 ) -> Result<String, RadrootsValidationReceiptError> { 288 receipt.validate()?; 289 serde_json::to_string(receipt).map_err(|_| RadrootsValidationReceiptError::InvalidJson) 290 } 291 292 pub fn validation_receipt_content_from_str( 293 content: &str, 294 ) -> Result<RadrootsTradeValidationReceipt, RadrootsValidationReceiptError> { 295 let receipt: RadrootsTradeValidationReceipt = 296 serde_json::from_str(content).map_err(|_| RadrootsValidationReceiptError::InvalidJson)?; 297 receipt.validate()?; 298 let canonical = validation_receipt_canonical_content(&receipt)?; 299 if canonical != content { 300 return Err(RadrootsValidationReceiptError::NonCanonicalJson); 301 } 302 Ok(receipt) 303 } 304 305 pub fn validation_receipt_tags( 306 order_id: &str, 307 receipt: &RadrootsTradeValidationReceipt, 308 ) -> Result<Vec<Vec<String>>, RadrootsValidationReceiptError> { 309 receipt.validate()?; 310 validate_required_str(order_id, "order_id")?; 311 Ok(vec![ 312 vec![TAG_D.to_string(), order_id.to_string()], 313 vec![ 314 "e".to_string(), 315 receipt.statement.listing_event_id.clone(), 316 String::new(), 317 String::new(), 318 "listing".to_string(), 319 ], 320 vec![ 321 "e".to_string(), 322 receipt.statement.root_event_id.clone(), 323 String::new(), 324 String::new(), 325 "root".to_string(), 326 ], 327 vec![ 328 "e".to_string(), 329 receipt.statement.target_event_id.clone(), 330 String::new(), 331 String::new(), 332 "target".to_string(), 333 ], 334 vec![ 335 TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT.to_string(), 336 receipt.event_set_root.clone(), 337 ], 338 vec![ 339 TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT.to_string(), 340 receipt.new_state_root.clone(), 341 ], 342 vec![ 343 TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH.to_string(), 344 receipt.public_values_hash.clone(), 345 ], 346 vec![ 347 TAG_VALIDATION_RECEIPT_PROOF_SYSTEM.to_string(), 348 receipt.proof.system.as_str().to_string(), 349 ], 350 vec![ 351 TAG_VALIDATION_RECEIPT_RECEIPT_TYPE.to_string(), 352 receipt.receipt_type.as_str().to_string(), 353 ], 354 ]) 355 } 356 357 pub fn validation_receipt_tags_from_tags( 358 tags: &[Vec<String>], 359 ) -> Result<RadrootsValidationReceiptTags, RadrootsValidationReceiptError> { 360 let order_id = required_tag_value(tags, TAG_D)?; 361 let listing_event_id = required_event_marker(tags, "listing")?; 362 let root_event_id = required_event_marker(tags, "root")?; 363 let target_event_id = required_event_marker(tags, "target")?; 364 let event_set_root = required_tag_value(tags, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; 365 let reducer_output_root = required_tag_value(tags, TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT)?; 366 let public_values_hash = required_tag_value(tags, TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH)?; 367 let proof_system = RadrootsValidationReceiptProofSystem::from_label(&required_tag_value( 368 tags, 369 TAG_VALIDATION_RECEIPT_PROOF_SYSTEM, 370 )?) 371 .ok_or(RadrootsValidationReceiptError::InvalidTag( 372 TAG_VALIDATION_RECEIPT_PROOF_SYSTEM, 373 ))?; 374 let receipt_type = RadrootsValidationReceiptType::from_label(&required_tag_value( 375 tags, 376 TAG_VALIDATION_RECEIPT_RECEIPT_TYPE, 377 )?) 378 .ok_or(RadrootsValidationReceiptError::InvalidTag( 379 TAG_VALIDATION_RECEIPT_RECEIPT_TYPE, 380 ))?; 381 382 validate_event_id(&listing_event_id, "tags.e.listing")?; 383 validate_event_id(&root_event_id, "tags.e.root")?; 384 validate_event_id(&target_event_id, "tags.e.target")?; 385 validate_hash32(&event_set_root, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; 386 validate_hash32( 387 &reducer_output_root, 388 TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT, 389 )?; 390 validate_hash32( 391 &public_values_hash, 392 TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH, 393 )?; 394 395 Ok(RadrootsValidationReceiptTags { 396 event_set_root, 397 listing_event_id, 398 order_id, 399 proof_system, 400 public_values_hash, 401 receipt_type, 402 reducer_output_root, 403 root_event_id, 404 target_event_id, 405 }) 406 } 407 408 pub fn validation_receipt_event_build( 409 order_id: &str, 410 receipt: &RadrootsTradeValidationReceipt, 411 ) -> Result<WireEventParts, RadrootsValidationReceiptError> { 412 Ok(WireEventParts { 413 kind: KIND_TRADE_VALIDATION_RECEIPT, 414 content: validation_receipt_canonical_content(receipt)?, 415 tags: validation_receipt_tags(order_id, receipt)?, 416 }) 417 } 418 419 pub fn validation_receipt_from_event( 420 event: &RadrootsNostrEvent, 421 ) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> { 422 verify_validation_receipt_event(event, RadrootsValidationReceiptExpectedBinding::default()) 423 } 424 425 pub fn verify_validation_receipt_event( 426 event: &RadrootsNostrEvent, 427 expected: RadrootsValidationReceiptExpectedBinding<'_>, 428 ) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> { 429 if event.kind != KIND_TRADE_VALIDATION_RECEIPT { 430 return Err(RadrootsValidationReceiptError::InvalidKind { 431 expected: KIND_TRADE_VALIDATION_RECEIPT, 432 got: event.kind, 433 }); 434 } 435 436 let receipt = validation_receipt_content_from_str(&event.content)?; 437 let tags = validation_receipt_tags_from_tags(&event.tags)?; 438 439 if tags.listing_event_id != receipt.statement.listing_event_id { 440 return Err(RadrootsValidationReceiptError::TagMismatch( 441 "listing_event_id", 442 )); 443 } 444 if tags.root_event_id != receipt.statement.root_event_id { 445 return Err(RadrootsValidationReceiptError::TagMismatch("root_event_id")); 446 } 447 if tags.target_event_id != receipt.statement.target_event_id { 448 return Err(RadrootsValidationReceiptError::TagMismatch( 449 "target_event_id", 450 )); 451 } 452 if tags.event_set_root != receipt.event_set_root { 453 return Err(RadrootsValidationReceiptError::TagMismatch( 454 "event_set_root", 455 )); 456 } 457 if tags.reducer_output_root != receipt.new_state_root { 458 return Err(RadrootsValidationReceiptError::TagMismatch( 459 "reducer_output_root", 460 )); 461 } 462 if tags.public_values_hash != receipt.public_values_hash { 463 return Err(RadrootsValidationReceiptError::TagMismatch( 464 "public_values_hash", 465 )); 466 } 467 if tags.proof_system != receipt.proof.system { 468 return Err(RadrootsValidationReceiptError::TagMismatch("proof_system")); 469 } 470 if tags.receipt_type != receipt.receipt_type { 471 return Err(RadrootsValidationReceiptError::TagMismatch("receipt_type")); 472 } 473 474 validate_expected_binding(&tags, &receipt, expected)?; 475 476 Ok(RadrootsVerifiedValidationReceipt { receipt, tags }) 477 } 478 479 fn validate_expected_binding( 480 tags: &RadrootsValidationReceiptTags, 481 receipt: &RadrootsTradeValidationReceipt, 482 expected: RadrootsValidationReceiptExpectedBinding<'_>, 483 ) -> Result<(), RadrootsValidationReceiptError> { 484 if let Some(order_id) = expected.order_id 485 && tags.order_id != order_id 486 { 487 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 488 "order_id", 489 )); 490 } 491 if let Some(listing_event_id) = expected.listing_event_id 492 && tags.listing_event_id != listing_event_id 493 { 494 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 495 "listing_event_id", 496 )); 497 } 498 if let Some(event_set_root) = expected.event_set_root 499 && tags.event_set_root != event_set_root 500 { 501 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 502 "event_set_root", 503 )); 504 } 505 if let Some(reducer_output_root) = expected.reducer_output_root 506 && tags.reducer_output_root != reducer_output_root 507 { 508 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 509 "reducer_output_root", 510 )); 511 } 512 if let Some(public_values_hash) = expected.public_values_hash 513 && tags.public_values_hash != public_values_hash 514 { 515 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 516 "public_values_hash", 517 )); 518 } 519 if let Some(proof_system) = expected.proof_system 520 && tags.proof_system != proof_system 521 { 522 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 523 "proof_system", 524 )); 525 } 526 if let Some(program_hash) = expected.program_hash 527 && receipt.proof.program_hash.as_deref() != Some(program_hash) 528 { 529 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 530 "program_hash", 531 )); 532 } 533 if let Some(verifying_key_hash) = expected.verifying_key_hash 534 && receipt.proof.verifying_key_hash.as_deref() != Some(verifying_key_hash) 535 { 536 return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 537 "verifying_key_hash", 538 )); 539 } 540 Ok(()) 541 } 542 543 fn required_tag_value( 544 tags: &[Vec<String>], 545 name: &'static str, 546 ) -> Result<String, RadrootsValidationReceiptError> { 547 let mut matches = tags 548 .iter() 549 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(name)); 550 let tag = matches 551 .next() 552 .ok_or(RadrootsValidationReceiptError::MissingTag(name))?; 553 if matches.next().is_some() { 554 return Err(RadrootsValidationReceiptError::InvalidTag(name)); 555 } 556 let value = tag 557 .get(1) 558 .ok_or(RadrootsValidationReceiptError::InvalidTag(name))?; 559 validate_required_str(value, name)?; 560 Ok(value.clone()) 561 } 562 563 fn required_event_marker( 564 tags: &[Vec<String>], 565 marker: &'static str, 566 ) -> Result<String, RadrootsValidationReceiptError> { 567 let mut matches = tags.iter().filter(|tag| { 568 tag.first().map(|value| value.as_str()) == Some("e") 569 && tag.get(4).map(|value| value.as_str()) == Some(marker) 570 }); 571 let tag = matches 572 .next() 573 .ok_or(RadrootsValidationReceiptError::MissingTag(marker))?; 574 if matches.next().is_some() { 575 return Err(RadrootsValidationReceiptError::InvalidTag(marker)); 576 } 577 let value = tag 578 .get(1) 579 .ok_or(RadrootsValidationReceiptError::InvalidTag(marker))?; 580 validate_required_str(value, marker)?; 581 Ok(value.clone()) 582 } 583 584 fn validate_required_option_hash32( 585 value: &Option<String>, 586 field: &'static str, 587 ) -> Result<(), RadrootsValidationReceiptError> { 588 match value { 589 Some(value) => validate_hash32(value, field), 590 None => Err(RadrootsValidationReceiptError::InvalidProofMetadata(field)), 591 } 592 } 593 594 fn validate_required_str( 595 value: &str, 596 field: &'static str, 597 ) -> Result<(), RadrootsValidationReceiptError> { 598 if value.trim().is_empty() { 599 return Err(RadrootsValidationReceiptError::EmptyField(field)); 600 } 601 Ok(()) 602 } 603 604 fn validate_inline_proof_base64(value: &str) -> Result<(), RadrootsValidationReceiptError> { 605 validate_required_str(value, "proof.inline_proof_base64")?; 606 base64::engine::general_purpose::STANDARD 607 .decode(value) 608 .map_err(|_| { 609 RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") 610 })?; 611 612 Ok(()) 613 } 614 615 fn validate_proof_reference(value: &str) -> Result<(), RadrootsValidationReceiptError> { 616 validate_required_str(value, "proof.proof_reference")?; 617 let digest = value 618 .strip_prefix(VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX) 619 .ok_or(RadrootsValidationReceiptError::InvalidProofMetadata( 620 "proof.proof_reference", 621 ))?; 622 if digest.len() != 64 || !is_lower_hex(digest) { 623 return Err(RadrootsValidationReceiptError::InvalidProofMetadata( 624 "proof.proof_reference", 625 )); 626 } 627 Ok(()) 628 } 629 630 fn validate_result_error_bitmap( 631 result: RadrootsValidationReceiptResult, 632 error_bitmap: &str, 633 ) -> Result<(), RadrootsValidationReceiptError> { 634 match result { 635 RadrootsValidationReceiptResult::Valid if error_bitmap != zero_error_bitmap() => { 636 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 637 } 638 RadrootsValidationReceiptResult::Invalid if error_bitmap == zero_error_bitmap() => { 639 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 640 } 641 _ => Ok(()), 642 } 643 } 644 645 fn validate_error_bitmap(value: &str) -> Result<(), RadrootsValidationReceiptError> { 646 if value.len() != 34 || !value.starts_with("0x") || !is_lower_hex(&value[2..]) { 647 return Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")); 648 } 649 Ok(()) 650 } 651 652 fn validate_hash32(value: &str, field: &'static str) -> Result<(), RadrootsValidationReceiptError> { 653 if value.len() != 66 || !value.starts_with("0x") || !is_lower_hex(&value[2..]) { 654 return Err(RadrootsValidationReceiptError::InvalidField(field)); 655 } 656 Ok(()) 657 } 658 659 fn validate_event_id( 660 value: &str, 661 field: &'static str, 662 ) -> Result<(), RadrootsValidationReceiptError> { 663 if value.len() != 64 || !is_lower_hex(value) { 664 return Err(RadrootsValidationReceiptError::InvalidField(field)); 665 } 666 Ok(()) 667 } 668 669 fn is_lower_hex(value: &str) -> bool { 670 value 671 .bytes() 672 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 673 } 674 675 fn zero_error_bitmap() -> &'static str { 676 "0x00000000000000000000000000000000" 677 } 678 679 #[cfg(test)] 680 mod tests { 681 use super::{ 682 RadrootsTradeValidationReceipt, RadrootsValidationReceiptError, 683 RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, 684 RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, 685 RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, 686 TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT, TAG_VALIDATION_RECEIPT_PROOF_SYSTEM, 687 TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH, TAG_VALIDATION_RECEIPT_RECEIPT_TYPE, 688 TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT, validation_receipt_canonical_content, 689 validation_receipt_content_from_str, validation_receipt_event_build, 690 validation_receipt_from_event, validation_receipt_public_values_hash_hex, 691 validation_receipt_tags, validation_receipt_tags_from_tags, 692 verify_validation_receipt_event, 693 }; 694 use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D}; 695 696 fn hash32(c: char) -> String { 697 format!("0x{}", c.to_string().repeat(64)) 698 } 699 700 fn event_id(c: char) -> String { 701 c.to_string().repeat(64) 702 } 703 704 fn sample_validation_receipt() -> RadrootsTradeValidationReceipt { 705 RadrootsTradeValidationReceipt { 706 changed_records_root: hash32('6'), 707 domain: "radroots.receipt".to_string(), 708 error_bitmap: "0x00000000000000000000000000000000".to_string(), 709 event_set_root: hash32('c'), 710 new_state_root: hash32('4'), 711 previous_state_root: hash32('3'), 712 proof: RadrootsValidationReceiptProof { 713 inline_proof_base64: None, 714 mode: None, 715 program_hash: None, 716 proof_reference: None, 717 system: RadrootsValidationReceiptProofSystem::None, 718 verifying_key_hash: None, 719 }, 720 public_values_hash: validation_receipt_public_values_hash_hex( 721 br#"{"schema_version":1}"#, 722 ), 723 receipt_type: RadrootsValidationReceiptType::TradeTransition, 724 result: RadrootsValidationReceiptResult::Valid, 725 statement: RadrootsValidationReceiptStatement { 726 listing_event_id: event_id('0'), 727 root_event_id: event_id('1'), 728 target_event_id: event_id('2'), 729 statement_type: RadrootsValidationReceiptType::TradeTransition, 730 }, 731 version: 1, 732 } 733 } 734 735 fn sample_sp1_reference_receipt() -> RadrootsTradeValidationReceipt { 736 let mut receipt = sample_validation_receipt(); 737 receipt.proof = RadrootsValidationReceiptProof { 738 inline_proof_base64: None, 739 mode: Some("core".to_string()), 740 program_hash: Some(hash32('a')), 741 proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), 742 system: RadrootsValidationReceiptProofSystem::Sp1Core, 743 verifying_key_hash: Some(hash32('b')), 744 }; 745 receipt 746 } 747 748 fn sample_validation_receipt_event() -> RadrootsNostrEvent { 749 let receipt = sample_validation_receipt(); 750 let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); 751 RadrootsNostrEvent { 752 id: event_id('9'), 753 author: event_id('a'), 754 created_at: 1, 755 kind: parts.kind, 756 tags: parts.tags, 757 content: parts.content, 758 sig: "signature".to_string(), 759 } 760 } 761 762 #[test] 763 fn validation_receipt_labels_cover_all_variants() { 764 assert_eq!( 765 RadrootsValidationReceiptType::ListingValidation.as_str(), 766 "listing_validation" 767 ); 768 assert_eq!( 769 RadrootsValidationReceiptType::TradeTransition.as_str(), 770 "trade_transition" 771 ); 772 assert_eq!( 773 RadrootsValidationReceiptType::InventoryState.as_str(), 774 "inventory_state" 775 ); 776 assert_eq!( 777 RadrootsValidationReceiptType::StateCheckpoint.as_str(), 778 "state_checkpoint" 779 ); 780 assert_eq!( 781 RadrootsValidationReceiptType::from_label("listing_validation"), 782 Some(RadrootsValidationReceiptType::ListingValidation) 783 ); 784 assert_eq!( 785 RadrootsValidationReceiptType::from_label("trade_transition"), 786 Some(RadrootsValidationReceiptType::TradeTransition) 787 ); 788 assert_eq!( 789 RadrootsValidationReceiptType::from_label("inventory_state"), 790 Some(RadrootsValidationReceiptType::InventoryState) 791 ); 792 assert_eq!( 793 RadrootsValidationReceiptType::from_label("state_checkpoint"), 794 Some(RadrootsValidationReceiptType::StateCheckpoint) 795 ); 796 assert_eq!(RadrootsValidationReceiptType::from_label("unknown"), None); 797 798 assert_eq!(RadrootsValidationReceiptProofSystem::None.as_str(), "none"); 799 assert_eq!( 800 RadrootsValidationReceiptProofSystem::Sp1Core.as_str(), 801 "sp1_core" 802 ); 803 assert_eq!( 804 RadrootsValidationReceiptProofSystem::Sp1Compressed.as_str(), 805 "sp1_compressed" 806 ); 807 assert_eq!( 808 RadrootsValidationReceiptProofSystem::Sp1Groth16.as_str(), 809 "sp1_groth16" 810 ); 811 assert_eq!( 812 RadrootsValidationReceiptProofSystem::Sp1Plonk.as_str(), 813 "sp1_plonk" 814 ); 815 assert_eq!( 816 RadrootsValidationReceiptProofSystem::from_label("none"), 817 Some(RadrootsValidationReceiptProofSystem::None) 818 ); 819 assert_eq!( 820 RadrootsValidationReceiptProofSystem::from_label("sp1_core"), 821 Some(RadrootsValidationReceiptProofSystem::Sp1Core) 822 ); 823 assert_eq!( 824 RadrootsValidationReceiptProofSystem::from_label("sp1_compressed"), 825 Some(RadrootsValidationReceiptProofSystem::Sp1Compressed) 826 ); 827 assert_eq!( 828 RadrootsValidationReceiptProofSystem::from_label("sp1_groth16"), 829 Some(RadrootsValidationReceiptProofSystem::Sp1Groth16) 830 ); 831 assert_eq!( 832 RadrootsValidationReceiptProofSystem::from_label("sp1_plonk"), 833 Some(RadrootsValidationReceiptProofSystem::Sp1Plonk) 834 ); 835 assert_eq!( 836 RadrootsValidationReceiptProofSystem::from_label("unknown"), 837 None 838 ); 839 assert_eq!( 840 RadrootsValidationReceiptProofSystem::None.expected_mode(), 841 None 842 ); 843 assert_eq!( 844 RadrootsValidationReceiptProofSystem::Sp1Core.expected_mode(), 845 Some("core") 846 ); 847 assert_eq!( 848 RadrootsValidationReceiptProofSystem::Sp1Compressed.expected_mode(), 849 Some("compressed") 850 ); 851 assert_eq!( 852 RadrootsValidationReceiptProofSystem::Sp1Groth16.expected_mode(), 853 Some("groth16") 854 ); 855 assert_eq!( 856 RadrootsValidationReceiptProofSystem::Sp1Plonk.expected_mode(), 857 Some("plonk") 858 ); 859 } 860 861 #[test] 862 fn validation_receipt_validate_rejects_core_field_errors() { 863 let mut receipt = sample_validation_receipt(); 864 receipt.version = 2; 865 assert_eq!( 866 receipt.validate(), 867 Err(RadrootsValidationReceiptError::InvalidField("version")) 868 ); 869 870 let mut receipt = sample_validation_receipt(); 871 receipt.domain = "other.domain".to_string(); 872 assert_eq!( 873 receipt.validate(), 874 Err(RadrootsValidationReceiptError::InvalidField("domain")) 875 ); 876 877 let mut receipt = sample_validation_receipt(); 878 receipt.statement.statement_type = RadrootsValidationReceiptType::ListingValidation; 879 assert_eq!( 880 receipt.validate(), 881 Err(RadrootsValidationReceiptError::InvalidField( 882 "statement.type" 883 )) 884 ); 885 886 let mut receipt = sample_validation_receipt(); 887 receipt.changed_records_root = "0x1".to_string(); 888 assert_eq!( 889 receipt.validate(), 890 Err(RadrootsValidationReceiptError::InvalidField( 891 "changed_records_root" 892 )) 893 ); 894 895 let mut receipt = sample_validation_receipt(); 896 receipt.event_set_root = format!("zz{}", "1".repeat(64)); 897 assert_eq!( 898 receipt.validate(), 899 Err(RadrootsValidationReceiptError::InvalidField( 900 "event_set_root" 901 )) 902 ); 903 904 let mut receipt = sample_validation_receipt(); 905 receipt.public_values_hash = format!("0x{}", "A".repeat(64)); 906 assert_eq!( 907 receipt.validate(), 908 Err(RadrootsValidationReceiptError::InvalidField( 909 "public_values_hash" 910 )) 911 ); 912 913 let mut receipt = sample_validation_receipt(); 914 receipt.error_bitmap = "0x1".to_string(); 915 assert_eq!( 916 receipt.validate(), 917 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 918 ); 919 920 let mut receipt = sample_validation_receipt(); 921 receipt.error_bitmap = format!("zz{}", "0".repeat(32)); 922 assert_eq!( 923 receipt.validate(), 924 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 925 ); 926 927 let mut receipt = sample_validation_receipt(); 928 receipt.error_bitmap = format!("0x{}", "A".repeat(32)); 929 assert_eq!( 930 receipt.validate(), 931 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 932 ); 933 934 let mut receipt = sample_validation_receipt(); 935 receipt.statement.listing_event_id = "bad".to_string(); 936 assert_eq!( 937 receipt.validate(), 938 Err(RadrootsValidationReceiptError::InvalidField( 939 "statement.listing_event_id" 940 )) 941 ); 942 943 let mut receipt = sample_validation_receipt(); 944 receipt.statement.root_event_id = "g".repeat(64); 945 assert_eq!( 946 receipt.validate(), 947 Err(RadrootsValidationReceiptError::InvalidField( 948 "statement.root_event_id" 949 )) 950 ); 951 952 let mut receipt = sample_validation_receipt(); 953 receipt.error_bitmap = "0x00000000000000000000000000000001".to_string(); 954 assert_eq!( 955 receipt.validate(), 956 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 957 ); 958 959 let mut receipt = sample_validation_receipt(); 960 receipt.result = RadrootsValidationReceiptResult::Invalid; 961 assert_eq!( 962 receipt.validate(), 963 Err(RadrootsValidationReceiptError::InvalidField("error_bitmap")) 964 ); 965 966 let mut receipt = sample_validation_receipt(); 967 receipt.result = RadrootsValidationReceiptResult::Invalid; 968 receipt.error_bitmap = "0x00000000000000000000000000000001".to_string(); 969 receipt 970 .validate() 971 .expect("invalid result with nonzero bitmap"); 972 } 973 974 #[test] 975 fn validation_receipt_proof_validation_covers_identity_modes_and_material_errors() { 976 let mut receipt = sample_validation_receipt(); 977 receipt.proof.mode = Some("core".to_string()); 978 assert_eq!( 979 receipt.validate(), 980 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 981 "proof.system" 982 )) 983 ); 984 985 let mut receipt = sample_validation_receipt(); 986 receipt.proof.program_hash = Some(hash32('a')); 987 assert_eq!( 988 receipt.validate(), 989 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 990 "proof.system" 991 )) 992 ); 993 994 let mut receipt = sample_validation_receipt(); 995 receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); 996 assert_eq!( 997 receipt.validate(), 998 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 999 "proof.system" 1000 )) 1001 ); 1002 1003 let mut receipt = sample_validation_receipt(); 1004 receipt.proof.verifying_key_hash = Some(hash32('b')); 1005 assert_eq!( 1006 receipt.validate(), 1007 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1008 "proof.system" 1009 )) 1010 ); 1011 1012 let mut missing_program = sample_sp1_reference_receipt(); 1013 missing_program.proof.program_hash = None; 1014 assert_eq!( 1015 missing_program.validate(), 1016 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1017 "proof.program_hash" 1018 )) 1019 ); 1020 1021 let mut missing_verifying_key = sample_sp1_reference_receipt(); 1022 missing_verifying_key.proof.verifying_key_hash = None; 1023 assert_eq!( 1024 missing_verifying_key.validate(), 1025 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1026 "proof.verifying_key_hash" 1027 )) 1028 ); 1029 1030 let mut wrong_mode = sample_sp1_reference_receipt(); 1031 wrong_mode.proof.mode = Some("compressed".to_string()); 1032 assert_eq!( 1033 wrong_mode.validate(), 1034 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1035 "proof.mode" 1036 )) 1037 ); 1038 1039 let mut empty_reference = sample_sp1_reference_receipt(); 1040 empty_reference.proof.proof_reference = Some(" ".to_string()); 1041 assert_eq!( 1042 empty_reference.validate(), 1043 Err(RadrootsValidationReceiptError::EmptyField( 1044 "proof.proof_reference" 1045 )) 1046 ); 1047 1048 let mut compressed = sample_sp1_reference_receipt(); 1049 compressed.proof.system = RadrootsValidationReceiptProofSystem::Sp1Compressed; 1050 compressed.proof.mode = Some("compressed".to_string()); 1051 compressed.validate().expect("compressed proof metadata"); 1052 1053 let mut groth16 = sample_sp1_reference_receipt(); 1054 groth16.proof.system = RadrootsValidationReceiptProofSystem::Sp1Groth16; 1055 groth16.proof.mode = Some("groth16".to_string()); 1056 groth16.validate().expect("groth16 proof metadata"); 1057 1058 let mut plonk = sample_sp1_reference_receipt(); 1059 plonk.proof.system = RadrootsValidationReceiptProofSystem::Sp1Plonk; 1060 plonk.proof.mode = Some("plonk".to_string()); 1061 plonk.validate().expect("plonk proof metadata"); 1062 } 1063 1064 #[test] 1065 fn validation_receipt_tag_parser_rejects_invalid_shapes_and_labels() { 1066 let tags = validation_receipt_tags("order-1", &sample_validation_receipt()).unwrap(); 1067 1068 let mut duplicate_order = tags.clone(); 1069 duplicate_order.push(vec![TAG_D.to_string(), "other-order".to_string()]); 1070 assert_eq!( 1071 validation_receipt_tags_from_tags(&duplicate_order), 1072 Err(RadrootsValidationReceiptError::InvalidTag(TAG_D)) 1073 ); 1074 1075 let mut malformed_order = tags.clone(); 1076 malformed_order[0] = vec![TAG_D.to_string()]; 1077 assert_eq!( 1078 validation_receipt_tags_from_tags(&malformed_order), 1079 Err(RadrootsValidationReceiptError::InvalidTag(TAG_D)) 1080 ); 1081 1082 let mut empty_order = tags.clone(); 1083 empty_order[0][1] = " ".to_string(); 1084 assert_eq!( 1085 validation_receipt_tags_from_tags(&empty_order), 1086 Err(RadrootsValidationReceiptError::EmptyField(TAG_D)) 1087 ); 1088 1089 let mut duplicate_listing = tags.clone(); 1090 duplicate_listing.push(vec![ 1091 "e".to_string(), 1092 event_id('3'), 1093 String::new(), 1094 String::new(), 1095 "listing".to_string(), 1096 ]); 1097 assert_eq!( 1098 validation_receipt_tags_from_tags(&duplicate_listing), 1099 Err(RadrootsValidationReceiptError::InvalidTag("listing")) 1100 ); 1101 1102 let mut empty_listing = tags.clone(); 1103 empty_listing[1][1] = " ".to_string(); 1104 assert_eq!( 1105 validation_receipt_tags_from_tags(&empty_listing), 1106 Err(RadrootsValidationReceiptError::EmptyField("listing")) 1107 ); 1108 1109 let mut invalid_listing = tags.clone(); 1110 invalid_listing[1][1] = "bad".to_string(); 1111 assert_eq!( 1112 validation_receipt_tags_from_tags(&invalid_listing), 1113 Err(RadrootsValidationReceiptError::InvalidField( 1114 "tags.e.listing" 1115 )) 1116 ); 1117 1118 let mut invalid_root = tags.clone(); 1119 invalid_root[2][1] = "g".repeat(64); 1120 assert_eq!( 1121 validation_receipt_tags_from_tags(&invalid_root), 1122 Err(RadrootsValidationReceiptError::InvalidField("tags.e.root")) 1123 ); 1124 1125 let mut invalid_target = tags.clone(); 1126 invalid_target[3][1] = "bad".to_string(); 1127 assert_eq!( 1128 validation_receipt_tags_from_tags(&invalid_target), 1129 Err(RadrootsValidationReceiptError::InvalidField( 1130 "tags.e.target" 1131 )) 1132 ); 1133 1134 let mut invalid_event_set = tags.clone(); 1135 invalid_event_set[4][1] = "bad".to_string(); 1136 assert_eq!( 1137 validation_receipt_tags_from_tags(&invalid_event_set), 1138 Err(RadrootsValidationReceiptError::InvalidField( 1139 TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT 1140 )) 1141 ); 1142 1143 let mut invalid_reducer = tags.clone(); 1144 invalid_reducer[5][1] = "bad".to_string(); 1145 assert_eq!( 1146 validation_receipt_tags_from_tags(&invalid_reducer), 1147 Err(RadrootsValidationReceiptError::InvalidField( 1148 TAG_VALIDATION_RECEIPT_REDUCER_OUTPUT_ROOT 1149 )) 1150 ); 1151 1152 let mut invalid_public_values = tags.clone(); 1153 invalid_public_values[6][1] = "bad".to_string(); 1154 assert_eq!( 1155 validation_receipt_tags_from_tags(&invalid_public_values), 1156 Err(RadrootsValidationReceiptError::InvalidField( 1157 TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH 1158 )) 1159 ); 1160 1161 let mut invalid_proof_system = tags.clone(); 1162 invalid_proof_system[7][1] = "sp1_unknown".to_string(); 1163 assert_eq!( 1164 validation_receipt_tags_from_tags(&invalid_proof_system), 1165 Err(RadrootsValidationReceiptError::InvalidTag( 1166 TAG_VALIDATION_RECEIPT_PROOF_SYSTEM 1167 )) 1168 ); 1169 1170 let mut invalid_receipt_type = tags.clone(); 1171 invalid_receipt_type[8][1] = "unknown".to_string(); 1172 assert_eq!( 1173 validation_receipt_tags_from_tags(&invalid_receipt_type), 1174 Err(RadrootsValidationReceiptError::InvalidTag( 1175 TAG_VALIDATION_RECEIPT_RECEIPT_TYPE 1176 )) 1177 ); 1178 } 1179 1180 #[test] 1181 fn validation_receipt_verifier_rejects_each_tag_mismatch() { 1182 let mut event = sample_validation_receipt_event(); 1183 event.tags[1][1] = event_id('3'); 1184 assert_eq!( 1185 validation_receipt_from_event(&event), 1186 Err(RadrootsValidationReceiptError::TagMismatch( 1187 "listing_event_id" 1188 )) 1189 ); 1190 1191 let mut event = sample_validation_receipt_event(); 1192 event.tags[2][1] = event_id('3'); 1193 assert_eq!( 1194 validation_receipt_from_event(&event), 1195 Err(RadrootsValidationReceiptError::TagMismatch("root_event_id")) 1196 ); 1197 1198 let mut event = sample_validation_receipt_event(); 1199 event.tags[3][1] = event_id('3'); 1200 assert_eq!( 1201 validation_receipt_from_event(&event), 1202 Err(RadrootsValidationReceiptError::TagMismatch( 1203 "target_event_id" 1204 )) 1205 ); 1206 1207 let mut event = sample_validation_receipt_event(); 1208 event.tags[4][1] = hash32('d'); 1209 assert_eq!( 1210 validation_receipt_from_event(&event), 1211 Err(RadrootsValidationReceiptError::TagMismatch( 1212 "event_set_root" 1213 )) 1214 ); 1215 1216 let mut event = sample_validation_receipt_event(); 1217 event.tags[5][1] = hash32('d'); 1218 assert_eq!( 1219 validation_receipt_from_event(&event), 1220 Err(RadrootsValidationReceiptError::TagMismatch( 1221 "reducer_output_root" 1222 )) 1223 ); 1224 1225 let mut event = sample_validation_receipt_event(); 1226 event.tags[6][1] = hash32('d'); 1227 assert_eq!( 1228 validation_receipt_from_event(&event), 1229 Err(RadrootsValidationReceiptError::TagMismatch( 1230 "public_values_hash" 1231 )) 1232 ); 1233 1234 let mut event = sample_validation_receipt_event(); 1235 event.tags[7][1] = "sp1_core".to_string(); 1236 assert_eq!( 1237 validation_receipt_from_event(&event), 1238 Err(RadrootsValidationReceiptError::TagMismatch("proof_system")) 1239 ); 1240 1241 let mut event = sample_validation_receipt_event(); 1242 event.tags[8][1] = "listing_validation".to_string(); 1243 assert_eq!( 1244 validation_receipt_from_event(&event), 1245 Err(RadrootsValidationReceiptError::TagMismatch("receipt_type")) 1246 ); 1247 } 1248 1249 #[test] 1250 fn validation_receipt_expected_binding_checks_all_supported_fields() { 1251 let event = sample_validation_receipt_event(); 1252 verify_validation_receipt_event( 1253 &event, 1254 RadrootsValidationReceiptExpectedBinding { 1255 event_set_root: Some(&hash32('c')), 1256 listing_event_id: Some(&event_id('0')), 1257 order_id: Some("order-1"), 1258 proof_system: Some(RadrootsValidationReceiptProofSystem::None), 1259 public_values_hash: Some(&validation_receipt_public_values_hash_hex( 1260 br#"{"schema_version":1}"#, 1261 )), 1262 reducer_output_root: Some(&hash32('4')), 1263 ..RadrootsValidationReceiptExpectedBinding::default() 1264 }, 1265 ) 1266 .expect("matching expected binding"); 1267 1268 assert_eq!( 1269 verify_validation_receipt_event( 1270 &event, 1271 RadrootsValidationReceiptExpectedBinding { 1272 listing_event_id: Some(&event_id('3')), 1273 ..RadrootsValidationReceiptExpectedBinding::default() 1274 }, 1275 ), 1276 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1277 "listing_event_id" 1278 )) 1279 ); 1280 assert_eq!( 1281 verify_validation_receipt_event( 1282 &event, 1283 RadrootsValidationReceiptExpectedBinding { 1284 event_set_root: Some(&hash32('d')), 1285 ..RadrootsValidationReceiptExpectedBinding::default() 1286 }, 1287 ), 1288 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1289 "event_set_root" 1290 )) 1291 ); 1292 assert_eq!( 1293 verify_validation_receipt_event( 1294 &event, 1295 RadrootsValidationReceiptExpectedBinding { 1296 reducer_output_root: Some(&hash32('d')), 1297 ..RadrootsValidationReceiptExpectedBinding::default() 1298 }, 1299 ), 1300 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1301 "reducer_output_root" 1302 )) 1303 ); 1304 assert_eq!( 1305 verify_validation_receipt_event( 1306 &event, 1307 RadrootsValidationReceiptExpectedBinding { 1308 public_values_hash: Some(&hash32('d')), 1309 ..RadrootsValidationReceiptExpectedBinding::default() 1310 }, 1311 ), 1312 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1313 "public_values_hash" 1314 )) 1315 ); 1316 assert_eq!( 1317 verify_validation_receipt_event( 1318 &event, 1319 RadrootsValidationReceiptExpectedBinding { 1320 proof_system: Some(RadrootsValidationReceiptProofSystem::Sp1Core), 1321 ..RadrootsValidationReceiptExpectedBinding::default() 1322 }, 1323 ), 1324 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1325 "proof_system" 1326 )) 1327 ); 1328 assert_eq!( 1329 verify_validation_receipt_event( 1330 &event, 1331 RadrootsValidationReceiptExpectedBinding { 1332 verifying_key_hash: Some(&hash32('b')), 1333 ..RadrootsValidationReceiptExpectedBinding::default() 1334 }, 1335 ), 1336 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1337 "verifying_key_hash" 1338 )) 1339 ); 1340 } 1341 1342 #[test] 1343 fn validation_receipt_round_trips_canonical_payload_and_tags() { 1344 let receipt = sample_validation_receipt(); 1345 let content = validation_receipt_canonical_content(&receipt).expect("canonical content"); 1346 assert_eq!( 1347 content, 1348 format!( 1349 "{{\"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}}", 1350 hash32('6'), 1351 hash32('c'), 1352 hash32('4'), 1353 hash32('3'), 1354 receipt.public_values_hash, 1355 event_id('0'), 1356 event_id('1'), 1357 event_id('2'), 1358 ) 1359 ); 1360 assert_eq!( 1361 validation_receipt_content_from_str(&content).expect("parsed content"), 1362 receipt 1363 ); 1364 1365 let event = sample_validation_receipt_event(); 1366 assert_eq!(event.kind, KIND_TRADE_VALIDATION_RECEIPT); 1367 let verified = validation_receipt_from_event(&event).expect("verified receipt"); 1368 assert_eq!(verified.tags.order_id, "order-1"); 1369 assert_eq!(verified.tags.listing_event_id, event_id('0')); 1370 assert_eq!(verified.tags.event_set_root, hash32('c')); 1371 assert_eq!(verified.tags.reducer_output_root, hash32('4')); 1372 assert_eq!( 1373 verified.tags.proof_system, 1374 RadrootsValidationReceiptProofSystem::None 1375 ); 1376 } 1377 1378 #[test] 1379 fn validation_receipt_public_values_hash_uses_domain_separator() { 1380 assert_ne!( 1381 validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#), 1382 validation_receipt_public_values_hash_hex(br#"{"schema_version":2}"#) 1383 ); 1384 assert_eq!( 1385 validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#), 1386 "0x0db3f9b2dbde90b932ea992c18bca5e4563b741258ed911c3c36fbbeeea88015" 1387 ); 1388 } 1389 1390 #[test] 1391 fn validation_receipt_verifier_rejects_non_validation_receipt_kind() { 1392 let mut event = sample_validation_receipt_event(); 1393 event.kind = 3434; 1394 assert_eq!( 1395 validation_receipt_from_event(&event), 1396 Err(RadrootsValidationReceiptError::InvalidKind { 1397 expected: KIND_TRADE_VALIDATION_RECEIPT, 1398 got: 3434 1399 }) 1400 ); 1401 } 1402 1403 #[test] 1404 fn validation_receipt_verifier_rejects_missing_and_wrong_bindings() { 1405 let event = sample_validation_receipt_event(); 1406 assert_eq!( 1407 verify_validation_receipt_event( 1408 &event, 1409 RadrootsValidationReceiptExpectedBinding { 1410 order_id: Some("other-order"), 1411 ..RadrootsValidationReceiptExpectedBinding::default() 1412 }, 1413 ), 1414 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1415 "order_id" 1416 )) 1417 ); 1418 1419 let mut missing_event_set = event.clone(); 1420 missing_event_set 1421 .tags 1422 .retain(|tag| tag.first().map(|value| value.as_str()) != Some("event_set_root")); 1423 assert_eq!( 1424 validation_receipt_from_event(&missing_event_set), 1425 Err(RadrootsValidationReceiptError::MissingTag("event_set_root")) 1426 ); 1427 1428 let mut wrong_reducer_output = event.clone(); 1429 let reducer_tag = wrong_reducer_output 1430 .tags 1431 .iter_mut() 1432 .find(|tag| tag.first().map(|value| value.as_str()) == Some("reducer_output_root")) 1433 .expect("reducer output tag"); 1434 reducer_tag[1] = hash32('8'); 1435 assert_eq!( 1436 validation_receipt_from_event(&wrong_reducer_output), 1437 Err(RadrootsValidationReceiptError::TagMismatch( 1438 "reducer_output_root" 1439 )) 1440 ); 1441 1442 let mut wrong_public_values = event.clone(); 1443 let public_values_tag = wrong_public_values 1444 .tags 1445 .iter_mut() 1446 .find(|tag| tag.first().map(|value| value.as_str()) == Some("public_values_hash")) 1447 .expect("public values tag"); 1448 public_values_tag[1] = hash32('b'); 1449 assert_eq!( 1450 validation_receipt_from_event(&wrong_public_values), 1451 Err(RadrootsValidationReceiptError::TagMismatch( 1452 "public_values_hash" 1453 )) 1454 ); 1455 } 1456 1457 #[test] 1458 fn validation_receipt_rejects_mismatched_proof_system_metadata() { 1459 let mut receipt = sample_validation_receipt(); 1460 receipt.proof = RadrootsValidationReceiptProof { 1461 inline_proof_base64: None, 1462 mode: Some("compressed".to_string()), 1463 program_hash: Some(hash32('a')), 1464 proof_reference: None, 1465 system: RadrootsValidationReceiptProofSystem::Sp1Compressed, 1466 verifying_key_hash: Some(hash32('b')), 1467 }; 1468 assert_eq!( 1469 receipt.validate(), 1470 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1471 "proof.material_missing" 1472 )) 1473 ); 1474 1475 receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); 1476 let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts"); 1477 let mut event = sample_validation_receipt_event(); 1478 event.content = parts.content; 1479 event.tags = parts.tags; 1480 let verified = verify_validation_receipt_event( 1481 &event, 1482 RadrootsValidationReceiptExpectedBinding { 1483 proof_system: Some(RadrootsValidationReceiptProofSystem::Sp1Compressed), 1484 ..RadrootsValidationReceiptExpectedBinding::default() 1485 }, 1486 ) 1487 .expect("sp1 receipt verifies with proof reference"); 1488 assert_eq!( 1489 verified.receipt.proof.system, 1490 RadrootsValidationReceiptProofSystem::Sp1Compressed 1491 ); 1492 } 1493 1494 #[test] 1495 fn validation_receipt_enforces_none_and_sp1_material_rules() { 1496 let mut none_with_material = sample_validation_receipt(); 1497 none_with_material.proof.inline_proof_base64 = Some("cHJvb2Y=".to_string()); 1498 assert_eq!( 1499 none_with_material.validate(), 1500 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1501 "proof.system" 1502 )) 1503 ); 1504 1505 let mut both_material_sources = sample_sp1_reference_receipt(); 1506 both_material_sources.proof.inline_proof_base64 = Some("cHJvb2Y=".to_string()); 1507 assert_eq!( 1508 both_material_sources.validate(), 1509 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1510 "proof.material_conflict" 1511 )) 1512 ); 1513 1514 let mut missing_material = sample_sp1_reference_receipt(); 1515 missing_material.proof.proof_reference = None; 1516 assert_eq!( 1517 missing_material.validate(), 1518 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1519 "proof.material_missing" 1520 )) 1521 ); 1522 } 1523 1524 #[test] 1525 fn validation_receipt_rejects_invalid_sp1_material_shape() { 1526 let mut invalid_inline = sample_sp1_reference_receipt(); 1527 invalid_inline.proof.proof_reference = None; 1528 invalid_inline.proof.inline_proof_base64 = Some("not canonical base64".to_string()); 1529 assert_eq!( 1530 invalid_inline.validate(), 1531 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1532 "proof.inline_proof_base64" 1533 )) 1534 ); 1535 1536 invalid_inline.proof.inline_proof_base64 = Some("cHJvb2Y=".to_string()); 1537 invalid_inline.validate().expect("valid inline proof shape"); 1538 1539 invalid_inline.proof.inline_proof_base64 = Some("AA==".to_string()); 1540 invalid_inline 1541 .validate() 1542 .expect("canonical zero byte inline proof shape"); 1543 1544 invalid_inline.proof.inline_proof_base64 = Some("AB==".to_string()); 1545 assert_eq!( 1546 invalid_inline.validate(), 1547 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1548 "proof.inline_proof_base64" 1549 )) 1550 ); 1551 1552 invalid_inline.proof.inline_proof_base64 = Some(String::new()); 1553 assert_eq!( 1554 invalid_inline.validate(), 1555 Err(RadrootsValidationReceiptError::EmptyField( 1556 "proof.inline_proof_base64" 1557 )) 1558 ); 1559 1560 let mut invalid_reference = sample_sp1_reference_receipt(); 1561 invalid_reference.proof.proof_reference = Some("https://example.test/proof".to_string()); 1562 assert_eq!( 1563 invalid_reference.validate(), 1564 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1565 "proof.proof_reference" 1566 )) 1567 ); 1568 1569 invalid_reference.proof.proof_reference = Some("radroots-proof://".to_string()); 1570 assert_eq!( 1571 invalid_reference.validate(), 1572 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1573 "proof.proof_reference" 1574 )) 1575 ); 1576 1577 invalid_reference.proof.proof_reference = 1578 Some(format!("radroots-proof://sha256/{}", "A".repeat(64))); 1579 assert_eq!( 1580 invalid_reference.validate(), 1581 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1582 "proof.proof_reference" 1583 )) 1584 ); 1585 1586 invalid_reference.proof.proof_reference = 1587 Some(format!("radroots-proof://sha256/{}", "1".repeat(63))); 1588 assert_eq!( 1589 invalid_reference.validate(), 1590 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1591 "proof.proof_reference" 1592 )) 1593 ); 1594 1595 invalid_reference.proof.proof_reference = 1596 Some(format!("radroots-proof://sha256/{}/proof", "1".repeat(64))); 1597 assert_eq!( 1598 invalid_reference.validate(), 1599 Err(RadrootsValidationReceiptError::InvalidProofMetadata( 1600 "proof.proof_reference" 1601 )) 1602 ); 1603 1604 invalid_reference.proof.proof_reference = 1605 Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); 1606 invalid_reference 1607 .validate() 1608 .expect("valid sha256 proof reference"); 1609 } 1610 1611 #[test] 1612 fn validation_receipt_expected_binding_enforces_sp1_identity() { 1613 let receipt = sample_sp1_reference_receipt(); 1614 let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts"); 1615 let mut event = sample_validation_receipt_event(); 1616 event.content = parts.content; 1617 event.tags = parts.tags; 1618 1619 verify_validation_receipt_event( 1620 &event, 1621 RadrootsValidationReceiptExpectedBinding { 1622 program_hash: Some(&hash32('a')), 1623 verifying_key_hash: Some(&hash32('b')), 1624 ..RadrootsValidationReceiptExpectedBinding::default() 1625 }, 1626 ) 1627 .expect("sp1 identity binding matches"); 1628 1629 assert_eq!( 1630 verify_validation_receipt_event( 1631 &event, 1632 RadrootsValidationReceiptExpectedBinding { 1633 program_hash: Some(&hash32('c')), 1634 ..RadrootsValidationReceiptExpectedBinding::default() 1635 }, 1636 ), 1637 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1638 "program_hash" 1639 )) 1640 ); 1641 assert_eq!( 1642 verify_validation_receipt_event( 1643 &event, 1644 RadrootsValidationReceiptExpectedBinding { 1645 verifying_key_hash: Some(&hash32('d')), 1646 ..RadrootsValidationReceiptExpectedBinding::default() 1647 }, 1648 ), 1649 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1650 "verifying_key_hash" 1651 )) 1652 ); 1653 1654 assert_eq!( 1655 verify_validation_receipt_event( 1656 &sample_validation_receipt_event(), 1657 RadrootsValidationReceiptExpectedBinding { 1658 program_hash: Some(&hash32('a')), 1659 ..RadrootsValidationReceiptExpectedBinding::default() 1660 }, 1661 ), 1662 Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( 1663 "program_hash" 1664 )) 1665 ); 1666 } 1667 1668 #[test] 1669 fn validation_receipt_rejects_malformed_canonical_json() { 1670 let receipt = sample_validation_receipt(); 1671 let pretty = serde_json::to_string_pretty(&receipt).expect("pretty json"); 1672 assert_eq!( 1673 validation_receipt_content_from_str(&pretty), 1674 Err(RadrootsValidationReceiptError::NonCanonicalJson) 1675 ); 1676 1677 let mut unknown_field = validation_receipt_canonical_content(&receipt).expect("canonical"); 1678 unknown_field.insert_str(1, "\"extra\":true,"); 1679 assert_eq!( 1680 validation_receipt_content_from_str(&unknown_field), 1681 Err(RadrootsValidationReceiptError::InvalidJson) 1682 ); 1683 } 1684 1685 #[test] 1686 fn validation_receipt_tag_builder_rejects_empty_order_id() { 1687 assert_eq!( 1688 validation_receipt_tags("", &sample_validation_receipt()), 1689 Err(RadrootsValidationReceiptError::EmptyField("order_id")) 1690 ); 1691 } 1692 }