lib

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

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 }