lib

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

lib.rs (67111B)


      1 #![cfg_attr(coverage_nightly, feature(coverage_attribute))]
      2 #![forbid(unsafe_code)]
      3 
      4 use serde::{Deserialize, Serialize};
      5 use sha2::{Digest, Sha256};
      6 use std::collections::BTreeMap;
      7 use thiserror::Error;
      8 
      9 pub const RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION: u32 = 1;
     10 pub const RADROOTS_SP1_TRADE_WITNESS_VERSION: u32 = 1;
     11 pub const RADROOTS_SP1_TRADE_PROTOCOL_VERSION: &str = "radroots.trade.v1";
     12 pub const RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH: &str =
     13     "0x3d8f7f463904d71f2d0d14b1551450756697e51c7b658e10c6d5c20a7bc61f08";
     14 pub const RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET: &str = "trade.order_acceptance.v1";
     15 pub const RADROOTS_SP1_TRADE_KIND_LISTING: u32 = 30402;
     16 pub const RADROOTS_SP1_TRADE_KIND_LISTING_DRAFT: u32 = 30403;
     17 pub const RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST: u32 = 3422;
     18 pub const RADROOTS_SP1_TRADE_KIND_ORDER_DECISION: u32 = 3423;
     19 
     20 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     21 #[serde(rename_all = "snake_case")]
     22 pub enum RadrootsSp1TradeProofStatementType {
     23     TradeTransition,
     24 }
     25 
     26 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     27 #[serde(rename_all = "snake_case")]
     28 pub enum RadrootsSp1TradeProofTransitionKind {
     29     OrderAccepted,
     30 }
     31 
     32 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     33 #[serde(rename_all = "snake_case")]
     34 pub enum RadrootsSp1TradeProofResult {
     35     Valid,
     36     Invalid,
     37 }
     38 
     39 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     40 #[serde(deny_unknown_fields)]
     41 pub struct RadrootsSp1TradeProofPublicValues {
     42     pub schema_version: u32,
     43     pub witness_version: u32,
     44     pub statement_type: RadrootsSp1TradeProofStatementType,
     45     pub proof_target: String,
     46     pub radroots_protocol_version: String,
     47     pub reducer_program_hash: String,
     48     pub sp1_program_hash: Option<String>,
     49     pub sp1_verifying_key_hash: Option<String>,
     50     pub event_set_root: String,
     51     pub listing_addr_hash: Option<String>,
     52     pub listing_event_id: Option<String>,
     53     pub order_id_hash: Option<String>,
     54     pub root_event_id: Option<String>,
     55     pub target_event_id: Option<String>,
     56     pub previous_state_root: String,
     57     pub new_state_root: String,
     58     pub transition: Option<RadrootsSp1TradeProofTransitionKind>,
     59     pub result: RadrootsSp1TradeProofResult,
     60     pub error_bitmap: String,
     61     pub inventory_delta_root: Option<String>,
     62     pub inventory_sequence: Option<u128>,
     63     pub inventory_prev_root: Option<String>,
     64     pub inventory_new_root: Option<String>,
     65     pub changed_records_root: String,
     66 }
     67 
     68 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     69 #[serde(deny_unknown_fields)]
     70 pub struct RadrootsSp1TradeInventoryBinWitness {
     71     pub bin_id: String,
     72     pub listing_capacity: u64,
     73     pub previous_reserved: u64,
     74 }
     75 
     76 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     77 #[serde(deny_unknown_fields)]
     78 pub struct RadrootsSp1TradeOrderItemWitness {
     79     pub bin_id: String,
     80     pub bin_count: u32,
     81 }
     82 
     83 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     84 #[serde(deny_unknown_fields)]
     85 pub struct RadrootsSp1TradeOrderRequestWitness {
     86     pub order_id: String,
     87     pub listing_addr: String,
     88     pub buyer_pubkey: String,
     89     pub seller_pubkey: String,
     90     pub items: Vec<RadrootsSp1TradeOrderItemWitness>,
     91 }
     92 
     93 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     94 #[serde(deny_unknown_fields)]
     95 pub struct RadrootsSp1TradeInventoryCommitmentWitness {
     96     pub bin_id: String,
     97     pub bin_count: u32,
     98 }
     99 
    100 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    101 pub enum RadrootsSp1TradeOrderDecisionWitness {
    102     Accepted {
    103         inventory_commitments: Vec<RadrootsSp1TradeInventoryCommitmentWitness>,
    104     },
    105     Declined {
    106         reason: String,
    107     },
    108 }
    109 
    110 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    111 #[serde(deny_unknown_fields)]
    112 pub struct RadrootsSp1TradeOrderDecisionEventWitness {
    113     pub order_id: String,
    114     pub listing_addr: String,
    115     pub buyer_pubkey: String,
    116     pub seller_pubkey: String,
    117     pub decision: RadrootsSp1TradeOrderDecisionWitness,
    118 }
    119 
    120 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
    121 #[serde(rename_all = "snake_case")]
    122 pub enum RadrootsSp1TradeEventEvidenceRole {
    123     Buyer,
    124     Seller,
    125 }
    126 
    127 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
    128 #[serde(rename_all = "snake_case")]
    129 pub enum RadrootsSp1TradeEventWorkflowPosition {
    130     Listing,
    131     OrderRequest,
    132     OrderDecision,
    133 }
    134 
    135 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    136 #[serde(deny_unknown_fields)]
    137 pub struct RadrootsSp1TradeCanonicalEventEvidence {
    138     pub event_id: String,
    139     pub signer_pubkey: String,
    140     pub kind: u32,
    141     pub canonical_event_hash: String,
    142     pub signature_hash: String,
    143     pub preverified_signature: bool,
    144     pub role: RadrootsSp1TradeEventEvidenceRole,
    145     pub workflow_position: RadrootsSp1TradeEventWorkflowPosition,
    146     pub content_hash: String,
    147     pub tags_hash: String,
    148     pub ordering_key: String,
    149 }
    150 
    151 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    152 #[serde(deny_unknown_fields)]
    153 pub struct RadrootsSp1TradeOrderAcceptanceWitness {
    154     pub witness_version: u32,
    155     pub proof_target: String,
    156     pub listing_event_id: String,
    157     pub request_event_id: String,
    158     pub decision_event_id: String,
    159     pub event_evidence: Vec<RadrootsSp1TradeCanonicalEventEvidence>,
    160     pub request: RadrootsSp1TradeOrderRequestWitness,
    161     pub decision: RadrootsSp1TradeOrderDecisionEventWitness,
    162     pub inventory_bins: Vec<RadrootsSp1TradeInventoryBinWitness>,
    163     pub inventory_sequence: u128,
    164     pub previous_state_root: Option<String>,
    165     pub reducer_program_hash: String,
    166     pub radroots_protocol_version: String,
    167     pub sp1_program_hash: Option<String>,
    168     pub sp1_verifying_key_hash: Option<String>,
    169 }
    170 
    171 #[derive(Clone, Debug, PartialEq, Eq)]
    172 pub struct RadrootsSp1TradePublicValuesExecution {
    173     pub public_values: RadrootsSp1TradeProofPublicValues,
    174     pub canonical_public_values: Vec<u8>,
    175     pub public_values_hash: String,
    176 }
    177 
    178 #[derive(Debug, Error, PartialEq, Eq)]
    179 pub enum RadrootsSp1TradeGuestError {
    180     #[error("{0} cannot be empty")]
    181     EmptyField(&'static str),
    182     #[error("invalid event id field {0}")]
    183     InvalidEventId(&'static str),
    184     #[error("invalid hash field {0}")]
    185     InvalidHash(&'static str),
    186     #[error("invalid order request")]
    187     InvalidOrderRequest,
    188     #[error("invalid order decision")]
    189     InvalidOrderDecision,
    190     #[error("unsupported witness version")]
    191     UnsupportedWitnessVersion,
    192     #[error("unsupported proof target")]
    193     UnsupportedProofTarget,
    194     #[error("unsupported protocol version")]
    195     UnsupportedProtocolVersion,
    196     #[error("unsupported reducer program hash")]
    197     UnsupportedReducerProgramHash,
    198     #[error("invalid event evidence field {0}")]
    199     InvalidEventEvidence(&'static str),
    200     #[error("missing event evidence {0}")]
    201     MissingEventEvidence(&'static str),
    202     #[error("duplicate event evidence {0}")]
    203     DuplicateEventEvidence(&'static str),
    204     #[error("event evidence signature is not preverified")]
    205     SignatureNotPreverified,
    206     #[error("unsupported event evidence kind {0}")]
    207     UnsupportedEventEvidenceKind(u32),
    208     #[error("event evidence field {0} does not match")]
    209     EventEvidenceBindingMismatch(&'static str),
    210     #[error("order decision is not accepted")]
    211     DecisionNotAccepted,
    212     #[error("order field {0} does not match")]
    213     OrderBindingMismatch(&'static str),
    214     #[error("inventory bin {0} is missing")]
    215     MissingInventoryBin(String),
    216     #[error("inventory bin {0} is duplicated")]
    217     DuplicateInventoryBin(String),
    218     #[error("inventory commitment does not match order request")]
    219     InventoryCommitmentMismatch,
    220     #[error("inventory bin {0} would overcommit listing capacity")]
    221     InventoryOvercommit(String),
    222     #[error("inventory quantity overflow")]
    223     InventoryOverflow,
    224     #[error("public values encoding failed")]
    225     PublicValuesEncoding,
    226 }
    227 
    228 pub fn reduce_order_acceptance_public_values(
    229     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    230 ) -> Result<RadrootsSp1TradePublicValuesExecution, RadrootsSp1TradeGuestError> {
    231     validate_witness_header(witness)?;
    232     validate_order_request_shape(&witness.request)?;
    233     validate_order_decision_shape(&witness.decision)?;
    234     validate_order_binding(witness)?;
    235     validate_event_evidence(witness)?;
    236 
    237     let request_counts = aggregate_requested_counts(&witness.request)?;
    238     let accepted_counts = aggregate_accepted_counts(&witness.decision)?;
    239     if request_counts != accepted_counts {
    240         return Err(RadrootsSp1TradeGuestError::InventoryCommitmentMismatch);
    241     }
    242 
    243     let inventory_bins = inventory_bins_by_id(&witness.inventory_bins)?;
    244     let next_inventory = apply_inventory_delta(&request_counts, &inventory_bins)?;
    245     let previous_state_root = witness
    246         .previous_state_root
    247         .clone()
    248         .unwrap_or_else(empty_state_root);
    249     validate_hash32(&previous_state_root, "previous_state_root")?;
    250 
    251     let event_set_root = event_evidence_set_root(&witness.event_evidence);
    252     let inventory_delta_root = hash_json("radroots:inventory-delta:v1", &request_counts);
    253     let inventory_prev_root = hash_json("radroots:inventory-prev:v1", &inventory_bins);
    254     let inventory_new_root = hash_json("radroots:inventory-new:v1", &next_inventory);
    255     let changed_records_root = hash_json(
    256         "radroots:changed-records:v1",
    257         &ChangedRecordsMaterial {
    258             order_id: &witness.request.order_id,
    259             listing_addr: &witness.request.listing_addr,
    260             target_event_id: &witness.decision_event_id,
    261             inventory_new_root: &inventory_new_root,
    262         },
    263     );
    264     let new_state_root = hash_json(
    265         "radroots:state-root:v1",
    266         &StateRootMaterial {
    267             previous_state_root: &previous_state_root,
    268             event_set_root: &event_set_root,
    269             changed_records_root: &changed_records_root,
    270             inventory_new_root: &inventory_new_root,
    271         },
    272     );
    273 
    274     let public_values = RadrootsSp1TradeProofPublicValues {
    275         schema_version: RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION,
    276         witness_version: witness.witness_version,
    277         statement_type: RadrootsSp1TradeProofStatementType::TradeTransition,
    278         proof_target: witness.proof_target.clone(),
    279         radroots_protocol_version: witness.radroots_protocol_version.clone(),
    280         reducer_program_hash: witness.reducer_program_hash.clone(),
    281         sp1_program_hash: witness.sp1_program_hash.clone(),
    282         sp1_verifying_key_hash: witness.sp1_verifying_key_hash.clone(),
    283         event_set_root,
    284         listing_addr_hash: Some(hash_bytes(
    285             "radroots:listing-addr:v1",
    286             witness.request.listing_addr.as_bytes(),
    287         )),
    288         listing_event_id: Some(witness.listing_event_id.clone()),
    289         order_id_hash: Some(hash_bytes(
    290             "radroots:order-id:v1",
    291             witness.request.order_id.as_bytes(),
    292         )),
    293         root_event_id: Some(witness.request_event_id.clone()),
    294         target_event_id: Some(witness.decision_event_id.clone()),
    295         previous_state_root,
    296         new_state_root,
    297         transition: Some(RadrootsSp1TradeProofTransitionKind::OrderAccepted),
    298         result: RadrootsSp1TradeProofResult::Valid,
    299         error_bitmap: zero_error_bitmap().to_string(),
    300         inventory_delta_root: Some(inventory_delta_root),
    301         inventory_sequence: Some(witness.inventory_sequence),
    302         inventory_prev_root: Some(inventory_prev_root),
    303         inventory_new_root: Some(inventory_new_root),
    304         changed_records_root,
    305     };
    306     let canonical_public_values = canonical_public_values_bytes(&public_values)?;
    307     let public_values_hash = validation_receipt_public_values_hash_hex(&canonical_public_values);
    308     Ok(RadrootsSp1TradePublicValuesExecution {
    309         public_values,
    310         canonical_public_values,
    311         public_values_hash,
    312     })
    313 }
    314 
    315 pub fn canonical_public_values_bytes(
    316     public_values: &RadrootsSp1TradeProofPublicValues,
    317 ) -> Result<Vec<u8>, RadrootsSp1TradeGuestError> {
    318     validate_public_values(public_values)?;
    319     serde_json::to_vec(public_values).map_err(|_| RadrootsSp1TradeGuestError::PublicValuesEncoding)
    320 }
    321 
    322 pub fn reduce_order_acceptance_canonical_public_values(
    323     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    324 ) -> Result<Vec<u8>, RadrootsSp1TradeGuestError> {
    325     Ok(reduce_order_acceptance_public_values(witness)?.canonical_public_values)
    326 }
    327 
    328 pub fn public_values_hash_hex(
    329     public_values: &RadrootsSp1TradeProofPublicValues,
    330 ) -> Result<String, RadrootsSp1TradeGuestError> {
    331     let bytes = canonical_public_values_bytes(public_values)?;
    332     Ok(validation_receipt_public_values_hash_hex(&bytes))
    333 }
    334 
    335 pub fn validation_receipt_public_values_hash_hex(public_values: &[u8]) -> String {
    336     let mut hasher = Sha256::new();
    337     hasher.update(b"radroots:sp1-public-values:v1");
    338     hasher.update(public_values);
    339     format!("0x{}", hex_lower(hasher.finalize().as_slice()))
    340 }
    341 
    342 pub fn empty_state_root() -> String {
    343     hash_bytes("radroots:state-empty:v1", &[])
    344 }
    345 
    346 fn validate_witness_header(
    347     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    348 ) -> Result<(), RadrootsSp1TradeGuestError> {
    349     if witness.witness_version != RADROOTS_SP1_TRADE_WITNESS_VERSION {
    350         return Err(RadrootsSp1TradeGuestError::UnsupportedWitnessVersion);
    351     }
    352     validate_required_str(&witness.proof_target, "proof_target")?;
    353     if witness.proof_target != RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET {
    354         return Err(RadrootsSp1TradeGuestError::UnsupportedProofTarget);
    355     }
    356     validate_event_id(&witness.listing_event_id, "listing_event_id")?;
    357     validate_event_id(&witness.request_event_id, "request_event_id")?;
    358     validate_event_id(&witness.decision_event_id, "decision_event_id")?;
    359     validate_required_str(&witness.reducer_program_hash, "reducer_program_hash")?;
    360     validate_hash32(&witness.reducer_program_hash, "reducer_program_hash")?;
    361     if witness.reducer_program_hash != RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH {
    362         return Err(RadrootsSp1TradeGuestError::UnsupportedReducerProgramHash);
    363     }
    364     validate_required_str(
    365         &witness.radroots_protocol_version,
    366         "radroots_protocol_version",
    367     )?;
    368     if witness.radroots_protocol_version != RADROOTS_SP1_TRADE_PROTOCOL_VERSION {
    369         return Err(RadrootsSp1TradeGuestError::UnsupportedProtocolVersion);
    370     }
    371     if let Some(hash) = &witness.sp1_verifying_key_hash {
    372         validate_hash32(hash, "sp1_verifying_key_hash")?;
    373     }
    374     if let Some(hash) = &witness.sp1_program_hash {
    375         validate_hash32(hash, "sp1_program_hash")?;
    376     }
    377     Ok(())
    378 }
    379 
    380 fn validate_event_evidence(
    381     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    382 ) -> Result<(), RadrootsSp1TradeGuestError> {
    383     if witness.event_evidence.is_empty() {
    384         return Err(RadrootsSp1TradeGuestError::MissingEventEvidence(
    385             "event_evidence",
    386         ));
    387     }
    388     if witness.event_evidence.len() != 3 {
    389         return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(
    390             "event_evidence.len",
    391         ));
    392     }
    393 
    394     let mut evidence_by_position = BTreeMap::new();
    395     for evidence in &witness.event_evidence {
    396         validate_event_id(&evidence.event_id, "event_evidence.event_id")?;
    397         validate_hex64(&evidence.signer_pubkey, "event_evidence.signer_pubkey")?;
    398         validate_hash32(
    399             &evidence.canonical_event_hash,
    400             "event_evidence.canonical_event_hash",
    401         )?;
    402         validate_hash32(&evidence.signature_hash, "event_evidence.signature_hash")?;
    403         validate_hash32(&evidence.content_hash, "event_evidence.content_hash")?;
    404         validate_hash32(&evidence.tags_hash, "event_evidence.tags_hash")?;
    405         validate_required_str(&evidence.ordering_key, "event_evidence.ordering_key")?;
    406         if !evidence.preverified_signature {
    407             return Err(RadrootsSp1TradeGuestError::SignatureNotPreverified);
    408         }
    409         if evidence_by_position
    410             .insert(evidence.workflow_position, evidence)
    411             .is_some()
    412         {
    413             return Err(RadrootsSp1TradeGuestError::DuplicateEventEvidence(
    414                 evidence.workflow_position.as_str(),
    415             ));
    416         }
    417     }
    418 
    419     let listing = evidence_by_position[&RadrootsSp1TradeEventWorkflowPosition::Listing];
    420     validate_evidence_binding(
    421         listing,
    422         &witness.listing_event_id,
    423         &witness.request.seller_pubkey,
    424         RadrootsSp1TradeEventEvidenceRole::Seller,
    425         &[
    426             RADROOTS_SP1_TRADE_KIND_LISTING,
    427             RADROOTS_SP1_TRADE_KIND_LISTING_DRAFT,
    428         ],
    429         "listing",
    430     )?;
    431 
    432     let request = evidence_by_position[&RadrootsSp1TradeEventWorkflowPosition::OrderRequest];
    433     validate_evidence_binding(
    434         request,
    435         &witness.request_event_id,
    436         &witness.request.buyer_pubkey,
    437         RadrootsSp1TradeEventEvidenceRole::Buyer,
    438         &[RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST],
    439         "order_request",
    440     )?;
    441 
    442     let decision = evidence_by_position[&RadrootsSp1TradeEventWorkflowPosition::OrderDecision];
    443     validate_evidence_binding(
    444         decision,
    445         &witness.decision_event_id,
    446         &witness.decision.seller_pubkey,
    447         RadrootsSp1TradeEventEvidenceRole::Seller,
    448         &[RADROOTS_SP1_TRADE_KIND_ORDER_DECISION],
    449         "order_decision",
    450     )?;
    451 
    452     Ok(())
    453 }
    454 
    455 fn validate_evidence_binding(
    456     evidence: &RadrootsSp1TradeCanonicalEventEvidence,
    457     expected_event_id: &str,
    458     expected_signer_pubkey: &str,
    459     expected_role: RadrootsSp1TradeEventEvidenceRole,
    460     allowed_kinds: &[u32],
    461     label: &'static str,
    462 ) -> Result<(), RadrootsSp1TradeGuestError> {
    463     if evidence.event_id != expected_event_id {
    464         return Err(RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch(
    465             label,
    466         ));
    467     }
    468     if evidence.signer_pubkey != expected_signer_pubkey {
    469         return Err(RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch(
    470             "signer_pubkey",
    471         ));
    472     }
    473     if evidence.role != expected_role {
    474         return Err(RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch(
    475             "role",
    476         ));
    477     }
    478     if !allowed_kinds.contains(&evidence.kind) {
    479         return Err(RadrootsSp1TradeGuestError::UnsupportedEventEvidenceKind(
    480             evidence.kind,
    481         ));
    482     }
    483     Ok(())
    484 }
    485 
    486 fn validate_order_request_shape(
    487     request: &RadrootsSp1TradeOrderRequestWitness,
    488 ) -> Result<(), RadrootsSp1TradeGuestError> {
    489     validate_required_str(&request.order_id, "request.order_id")?;
    490     validate_required_str(&request.listing_addr, "request.listing_addr")?;
    491     validate_required_str(&request.buyer_pubkey, "request.buyer_pubkey")?;
    492     validate_required_str(&request.seller_pubkey, "request.seller_pubkey")?;
    493     if request.items.is_empty() {
    494         return Err(RadrootsSp1TradeGuestError::InvalidOrderRequest);
    495     }
    496     for item in &request.items {
    497         validate_required_str(&item.bin_id, "request.items.bin_id")?;
    498         if item.bin_count == 0 {
    499             return Err(RadrootsSp1TradeGuestError::InvalidOrderRequest);
    500         }
    501     }
    502     Ok(())
    503 }
    504 
    505 fn validate_order_decision_shape(
    506     decision: &RadrootsSp1TradeOrderDecisionEventWitness,
    507 ) -> Result<(), RadrootsSp1TradeGuestError> {
    508     validate_required_str(&decision.order_id, "decision.order_id")?;
    509     validate_required_str(&decision.listing_addr, "decision.listing_addr")?;
    510     validate_required_str(&decision.buyer_pubkey, "decision.buyer_pubkey")?;
    511     validate_required_str(&decision.seller_pubkey, "decision.seller_pubkey")?;
    512     match &decision.decision {
    513         RadrootsSp1TradeOrderDecisionWitness::Accepted {
    514             inventory_commitments,
    515         } => {
    516             if inventory_commitments.is_empty() {
    517                 return Err(RadrootsSp1TradeGuestError::InvalidOrderDecision);
    518             }
    519             for commitment in inventory_commitments {
    520                 validate_required_str(&commitment.bin_id, "decision.inventory_commitments.bin_id")?;
    521                 if commitment.bin_count == 0 {
    522                     return Err(RadrootsSp1TradeGuestError::InvalidOrderDecision);
    523                 }
    524             }
    525             Ok(())
    526         }
    527         RadrootsSp1TradeOrderDecisionWitness::Declined { reason } => {
    528             validate_required_str(reason, "decision.reason")?;
    529             Ok(())
    530         }
    531     }
    532 }
    533 
    534 fn validate_order_binding(
    535     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    536 ) -> Result<(), RadrootsSp1TradeGuestError> {
    537     if !matches!(
    538         witness.decision.decision,
    539         RadrootsSp1TradeOrderDecisionWitness::Accepted { .. }
    540     ) {
    541         return Err(RadrootsSp1TradeGuestError::DecisionNotAccepted);
    542     }
    543     if witness.request.order_id != witness.decision.order_id {
    544         return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch("order_id"));
    545     }
    546     if witness.request.listing_addr != witness.decision.listing_addr {
    547         return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch(
    548             "listing_addr",
    549         ));
    550     }
    551     if witness.request.buyer_pubkey != witness.decision.buyer_pubkey {
    552         return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch(
    553             "buyer_pubkey",
    554         ));
    555     }
    556     if witness.request.seller_pubkey != witness.decision.seller_pubkey {
    557         return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch(
    558             "seller_pubkey",
    559         ));
    560     }
    561     Ok(())
    562 }
    563 
    564 fn aggregate_requested_counts(
    565     request: &RadrootsSp1TradeOrderRequestWitness,
    566 ) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> {
    567     let mut counts = BTreeMap::new();
    568     for item in &request.items {
    569         let entry = counts.entry(item.bin_id.clone()).or_insert(0u64);
    570         *entry = entry
    571             .checked_add(u64::from(item.bin_count))
    572             .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?;
    573     }
    574     Ok(counts)
    575 }
    576 
    577 fn aggregate_accepted_counts(
    578     decision: &RadrootsSp1TradeOrderDecisionEventWitness,
    579 ) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> {
    580     let RadrootsSp1TradeOrderDecisionWitness::Accepted {
    581         inventory_commitments,
    582     } = &decision.decision
    583     else {
    584         return Err(RadrootsSp1TradeGuestError::DecisionNotAccepted);
    585     };
    586     let mut counts = BTreeMap::new();
    587     for commitment in inventory_commitments {
    588         let entry = counts.entry(commitment.bin_id.clone()).or_insert(0u64);
    589         *entry = entry
    590             .checked_add(u64::from(commitment.bin_count))
    591             .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?;
    592     }
    593     Ok(counts)
    594 }
    595 
    596 fn inventory_bins_by_id(
    597     bins: &[RadrootsSp1TradeInventoryBinWitness],
    598 ) -> Result<BTreeMap<String, RadrootsSp1TradeInventoryBinWitness>, RadrootsSp1TradeGuestError> {
    599     let mut result = BTreeMap::new();
    600     for bin in bins {
    601         validate_required_str(&bin.bin_id, "inventory_bins.bin_id")?;
    602         if result.insert(bin.bin_id.clone(), bin.clone()).is_some() {
    603             return Err(RadrootsSp1TradeGuestError::DuplicateInventoryBin(
    604                 bin.bin_id.clone(),
    605             ));
    606         }
    607     }
    608     Ok(result)
    609 }
    610 
    611 fn apply_inventory_delta(
    612     request_counts: &BTreeMap<String, u64>,
    613     bins: &BTreeMap<String, RadrootsSp1TradeInventoryBinWitness>,
    614 ) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> {
    615     let mut next = BTreeMap::new();
    616     for (bin_id, requested) in request_counts {
    617         let bin = bins
    618             .get(bin_id)
    619             .ok_or_else(|| RadrootsSp1TradeGuestError::MissingInventoryBin(bin_id.clone()))?;
    620         let reserved = bin
    621             .previous_reserved
    622             .checked_add(*requested)
    623             .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?;
    624         if reserved > bin.listing_capacity {
    625             return Err(RadrootsSp1TradeGuestError::InventoryOvercommit(
    626                 bin_id.clone(),
    627             ));
    628         }
    629         next.insert(bin_id.clone(), reserved);
    630     }
    631     Ok(next)
    632 }
    633 
    634 fn validate_public_values(
    635     public_values: &RadrootsSp1TradeProofPublicValues,
    636 ) -> Result<(), RadrootsSp1TradeGuestError> {
    637     if public_values.schema_version != RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION {
    638         return Err(RadrootsSp1TradeGuestError::InvalidHash("schema_version"));
    639     }
    640     if public_values.witness_version != RADROOTS_SP1_TRADE_WITNESS_VERSION {
    641         return Err(RadrootsSp1TradeGuestError::UnsupportedWitnessVersion);
    642     }
    643     validate_required_str(&public_values.proof_target, "proof_target")?;
    644     if public_values.proof_target != RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET {
    645         return Err(RadrootsSp1TradeGuestError::UnsupportedProofTarget);
    646     }
    647     validate_required_str(
    648         &public_values.radroots_protocol_version,
    649         "radroots_protocol_version",
    650     )?;
    651     if public_values.radroots_protocol_version != RADROOTS_SP1_TRADE_PROTOCOL_VERSION {
    652         return Err(RadrootsSp1TradeGuestError::UnsupportedProtocolVersion);
    653     }
    654     validate_hash32(&public_values.reducer_program_hash, "reducer_program_hash")?;
    655     if public_values.reducer_program_hash != RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH {
    656         return Err(RadrootsSp1TradeGuestError::UnsupportedReducerProgramHash);
    657     }
    658     if let Some(hash) = &public_values.sp1_program_hash {
    659         validate_hash32(hash, "sp1_program_hash")?;
    660     }
    661     if let Some(hash) = &public_values.sp1_verifying_key_hash {
    662         validate_hash32(hash, "sp1_verifying_key_hash")?;
    663     }
    664     validate_hash32(&public_values.event_set_root, "event_set_root")?;
    665     if let Some(hash) = &public_values.listing_addr_hash {
    666         validate_hash32(hash, "listing_addr_hash")?;
    667     }
    668     if let Some(event_id) = &public_values.listing_event_id {
    669         validate_event_id(event_id, "listing_event_id")?;
    670     }
    671     if let Some(hash) = &public_values.order_id_hash {
    672         validate_hash32(hash, "order_id_hash")?;
    673     }
    674     if let Some(event_id) = &public_values.root_event_id {
    675         validate_event_id(event_id, "root_event_id")?;
    676     }
    677     if let Some(event_id) = &public_values.target_event_id {
    678         validate_event_id(event_id, "target_event_id")?;
    679     }
    680     validate_hash32(&public_values.previous_state_root, "previous_state_root")?;
    681     validate_hash32(&public_values.new_state_root, "new_state_root")?;
    682     validate_hash32(&public_values.changed_records_root, "changed_records_root")?;
    683     if public_values.error_bitmap != zero_error_bitmap() {
    684         return Err(RadrootsSp1TradeGuestError::InvalidHash("error_bitmap"));
    685     }
    686     if let Some(hash) = &public_values.inventory_delta_root {
    687         validate_hash32(hash, "inventory_delta_root")?;
    688     }
    689     if let Some(hash) = &public_values.inventory_prev_root {
    690         validate_hash32(hash, "inventory_prev_root")?;
    691     }
    692     if let Some(hash) = &public_values.inventory_new_root {
    693         validate_hash32(hash, "inventory_new_root")?;
    694     }
    695     Ok(())
    696 }
    697 
    698 fn event_evidence_set_root(evidence: &[RadrootsSp1TradeCanonicalEventEvidence]) -> String {
    699     let mut sorted = evidence.to_vec();
    700     sorted.sort_by(compare_event_evidence_order);
    701     hash_json("radroots:event-evidence-set:v1", &sorted)
    702 }
    703 
    704 fn compare_event_evidence_order(
    705     left: &RadrootsSp1TradeCanonicalEventEvidence,
    706     right: &RadrootsSp1TradeCanonicalEventEvidence,
    707 ) -> std::cmp::Ordering {
    708     let ordering = left.ordering_key.cmp(&right.ordering_key);
    709     if ordering == std::cmp::Ordering::Equal {
    710         left.event_id.cmp(&right.event_id)
    711     } else {
    712         ordering
    713     }
    714 }
    715 
    716 fn hash_json<T: Serialize>(domain: &'static str, value: &T) -> String {
    717     let bytes = serde_json::to_vec(value).expect("hash material serialization");
    718     hash_bytes(domain, &bytes)
    719 }
    720 
    721 fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String {
    722     let mut hasher = Sha256::new();
    723     hasher.update(domain.as_bytes());
    724     hasher.update(bytes);
    725     format!("0x{}", hex_lower(hasher.finalize().as_slice()))
    726 }
    727 
    728 fn validate_required_str(
    729     value: &str,
    730     field: &'static str,
    731 ) -> Result<(), RadrootsSp1TradeGuestError> {
    732     if value.trim().is_empty() {
    733         return Err(RadrootsSp1TradeGuestError::EmptyField(field));
    734     }
    735     Ok(())
    736 }
    737 
    738 fn validate_hash32(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> {
    739     if value.len() != 66 || !value.starts_with("0x") || !is_lower_hex(&value[2..]) {
    740         return Err(RadrootsSp1TradeGuestError::InvalidHash(field));
    741     }
    742     Ok(())
    743 }
    744 
    745 fn validate_event_id(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> {
    746     if value.len() != 64 || !is_lower_hex(value) {
    747         return Err(RadrootsSp1TradeGuestError::InvalidEventId(field));
    748     }
    749     Ok(())
    750 }
    751 
    752 fn validate_hex64(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> {
    753     if value.len() != 64 || !is_lower_hex(value) {
    754         return Err(RadrootsSp1TradeGuestError::InvalidEventEvidence(field));
    755     }
    756     Ok(())
    757 }
    758 
    759 impl RadrootsSp1TradeEventWorkflowPosition {
    760     const fn as_str(self) -> &'static str {
    761         match self {
    762             Self::Listing => "listing",
    763             Self::OrderRequest => "order_request",
    764             Self::OrderDecision => "order_decision",
    765         }
    766     }
    767 }
    768 
    769 fn is_lower_hex(value: &str) -> bool {
    770     value
    771         .bytes()
    772         .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    773 }
    774 
    775 fn hex_lower(bytes: &[u8]) -> String {
    776     const HEX: &[u8; 16] = b"0123456789abcdef";
    777     let mut out = String::with_capacity(bytes.len() * 2);
    778     for byte in bytes {
    779         out.push(HEX[(byte >> 4) as usize] as char);
    780         out.push(HEX[(byte & 0x0f) as usize] as char);
    781     }
    782     out
    783 }
    784 
    785 fn zero_error_bitmap() -> &'static str {
    786     "0x00000000000000000000000000000000"
    787 }
    788 
    789 #[derive(Serialize)]
    790 struct ChangedRecordsMaterial<'a> {
    791     order_id: &'a str,
    792     listing_addr: &'a str,
    793     target_event_id: &'a str,
    794     inventory_new_root: &'a str,
    795 }
    796 
    797 #[derive(Serialize)]
    798 struct StateRootMaterial<'a> {
    799     previous_state_root: &'a str,
    800     event_set_root: &'a str,
    801     changed_records_root: &'a str,
    802     inventory_new_root: &'a str,
    803 }
    804 
    805 #[cfg(test)]
    806 #[cfg_attr(coverage_nightly, coverage(off))]
    807 mod tests {
    808     use super::{
    809         RADROOTS_SP1_TRADE_KIND_LISTING, RADROOTS_SP1_TRADE_KIND_LISTING_DRAFT,
    810         RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST,
    811         RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, RADROOTS_SP1_TRADE_PROTOCOL_VERSION,
    812         RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, RADROOTS_SP1_TRADE_WITNESS_VERSION,
    813         RadrootsSp1TradeCanonicalEventEvidence, RadrootsSp1TradeEventEvidenceRole,
    814         RadrootsSp1TradeEventWorkflowPosition, RadrootsSp1TradeGuestError,
    815         RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeInventoryCommitmentWitness,
    816         RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeOrderDecisionEventWitness,
    817         RadrootsSp1TradeOrderDecisionWitness, RadrootsSp1TradeOrderItemWitness,
    818         RadrootsSp1TradeOrderRequestWitness, RadrootsSp1TradeProofResult,
    819         RadrootsSp1TradeProofTransitionKind, canonical_public_values_bytes, public_values_hash_hex,
    820         reduce_order_acceptance_canonical_public_values, reduce_order_acceptance_public_values,
    821     };
    822 
    823     fn witness() -> RadrootsSp1TradeOrderAcceptanceWitness {
    824         RadrootsSp1TradeOrderAcceptanceWitness {
    825             witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION,
    826             proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(),
    827             listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    828                 .to_string(),
    829             request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    830                 .to_string(),
    831             decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    832                 .to_string(),
    833             event_evidence: event_evidence(),
    834             request: request(2),
    835             decision: decision(2),
    836             inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness {
    837                 bin_id: "bin-1".to_string(),
    838                 listing_capacity: 5,
    839                 previous_reserved: 1,
    840             }],
    841             inventory_sequence: 7,
    842             previous_state_root: None,
    843             reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
    844             radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
    845             sp1_program_hash: Some(
    846                 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
    847             ),
    848             sp1_verifying_key_hash: Some(
    849                 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
    850             ),
    851         }
    852     }
    853 
    854     fn event_evidence() -> Vec<RadrootsSp1TradeCanonicalEventEvidence> {
    855         vec![
    856             RadrootsSp1TradeCanonicalEventEvidence {
    857                 event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    858                     .to_string(),
    859                 signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    860                     .to_string(),
    861                 kind: RADROOTS_SP1_TRADE_KIND_LISTING,
    862                 canonical_event_hash:
    863                     "0x1010101010101010101010101010101010101010101010101010101010101010".to_string(),
    864                 signature_hash:
    865                     "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
    866                 preverified_signature: true,
    867                 role: RadrootsSp1TradeEventEvidenceRole::Seller,
    868                 workflow_position: RadrootsSp1TradeEventWorkflowPosition::Listing,
    869                 content_hash: "0x1212121212121212121212121212121212121212121212121212121212121212"
    870                     .to_string(),
    871                 tags_hash: "0x1313131313131313131313131313131313131313131313131313131313131313"
    872                     .to_string(),
    873                 ordering_key: "001:listing".to_string(),
    874             },
    875             RadrootsSp1TradeCanonicalEventEvidence {
    876                 event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    877                     .to_string(),
    878                 signer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    879                     .to_string(),
    880                 kind: RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST,
    881                 canonical_event_hash:
    882                     "0x2020202020202020202020202020202020202020202020202020202020202020".to_string(),
    883                 signature_hash:
    884                     "0x2121212121212121212121212121212121212121212121212121212121212121".to_string(),
    885                 preverified_signature: true,
    886                 role: RadrootsSp1TradeEventEvidenceRole::Buyer,
    887                 workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderRequest,
    888                 content_hash: "0x2222222222222222222222222222222222222222222222222222222222222222"
    889                     .to_string(),
    890                 tags_hash: "0x2323232323232323232323232323232323232323232323232323232323232323"
    891                     .to_string(),
    892                 ordering_key: "002:order_request".to_string(),
    893             },
    894             RadrootsSp1TradeCanonicalEventEvidence {
    895                 event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    896                     .to_string(),
    897                 signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    898                     .to_string(),
    899                 kind: RADROOTS_SP1_TRADE_KIND_ORDER_DECISION,
    900                 canonical_event_hash:
    901                     "0x3030303030303030303030303030303030303030303030303030303030303030".to_string(),
    902                 signature_hash:
    903                     "0x3131313131313131313131313131313131313131313131313131313131313131".to_string(),
    904                 preverified_signature: true,
    905                 role: RadrootsSp1TradeEventEvidenceRole::Seller,
    906                 workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderDecision,
    907                 content_hash: "0x3232323232323232323232323232323232323232323232323232323232323232"
    908                     .to_string(),
    909                 tags_hash: "0x3333333333333333333333333333333333333333333333333333333333333333"
    910                     .to_string(),
    911                 ordering_key: "003:order_decision".to_string(),
    912             },
    913         ]
    914     }
    915 
    916     fn request(bin_count: u32) -> RadrootsSp1TradeOrderRequestWitness {
    917         RadrootsSp1TradeOrderRequestWitness {
    918             order_id: "order-1".to_string(),
    919             listing_addr:
    920                 "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1"
    921                     .to_string(),
    922             buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    923                 .to_string(),
    924             seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    925                 .to_string(),
    926             items: vec![RadrootsSp1TradeOrderItemWitness {
    927                 bin_id: "bin-1".to_string(),
    928                 bin_count,
    929             }],
    930         }
    931     }
    932 
    933     fn decision(bin_count: u32) -> RadrootsSp1TradeOrderDecisionEventWitness {
    934         RadrootsSp1TradeOrderDecisionEventWitness {
    935             order_id: "order-1".to_string(),
    936             listing_addr:
    937                 "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1"
    938                     .to_string(),
    939             buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    940                 .to_string(),
    941             seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    942                 .to_string(),
    943             decision: RadrootsSp1TradeOrderDecisionWitness::Accepted {
    944                 inventory_commitments: vec![RadrootsSp1TradeInventoryCommitmentWitness {
    945                     bin_id: "bin-1".to_string(),
    946                     bin_count,
    947                 }],
    948             },
    949         }
    950     }
    951 
    952     #[test]
    953     fn order_acceptance_public_values_are_deterministic() {
    954         let left = reduce_order_acceptance_public_values(&witness()).expect("left execution");
    955         let right = reduce_order_acceptance_public_values(&witness()).expect("right execution");
    956         assert_eq!(left.public_values, right.public_values);
    957         assert_eq!(left.canonical_public_values, right.canonical_public_values);
    958         assert_eq!(left.public_values_hash, right.public_values_hash);
    959         assert_eq!(
    960             left.public_values.transition,
    961             Some(RadrootsSp1TradeProofTransitionKind::OrderAccepted)
    962         );
    963         assert_eq!(
    964             left.public_values.result,
    965             RadrootsSp1TradeProofResult::Valid
    966         );
    967         assert_eq!(
    968             left.public_values.root_event_id.as_deref(),
    969             Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
    970         );
    971         assert_eq!(
    972             left.public_values.target_event_id.as_deref(),
    973             Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
    974         );
    975     }
    976 
    977     #[test]
    978     fn public_values_canonical_bytes_reencode_identically() {
    979         let execution = reduce_order_acceptance_public_values(&witness()).expect("execution");
    980         let decoded: super::RadrootsSp1TradeProofPublicValues =
    981             serde_json::from_slice(&execution.canonical_public_values).expect("decode");
    982         let encoded = canonical_public_values_bytes(&decoded).expect("reencode");
    983         assert_eq!(execution.canonical_public_values, encoded);
    984         assert_eq!(
    985             public_values_hash_hex(&decoded).expect("hash"),
    986             execution.public_values_hash
    987         );
    988     }
    989 
    990     #[test]
    991     fn guest_public_values_output_is_canonical_bytes() {
    992         let execution = reduce_order_acceptance_public_values(&witness()).expect("execution");
    993         let bytes =
    994             reduce_order_acceptance_canonical_public_values(&witness()).expect("guest bytes");
    995         assert_eq!(bytes, execution.canonical_public_values);
    996     }
    997 
    998     #[test]
    999     fn overcommitted_inventory_is_rejected() {
   1000         let mut input = witness();
   1001         input.inventory_bins[0].listing_capacity = 2;
   1002         let err = reduce_order_acceptance_public_values(&input).expect_err("overcommit");
   1003         assert_eq!(
   1004             err,
   1005             RadrootsSp1TradeGuestError::InventoryOvercommit("bin-1".to_string())
   1006         );
   1007     }
   1008 
   1009     #[test]
   1010     fn mismatched_commitment_is_rejected() {
   1011         let mut input = witness();
   1012         input.decision = decision(1);
   1013         let err = reduce_order_acceptance_public_values(&input).expect_err("mismatch");
   1014         assert_eq!(err, RadrootsSp1TradeGuestError::InventoryCommitmentMismatch);
   1015     }
   1016 
   1017     #[test]
   1018     fn parsed_only_witness_is_rejected() {
   1019         let mut input = witness();
   1020         input.event_evidence.clear();
   1021         let err = reduce_order_acceptance_public_values(&input).expect_err("missing evidence");
   1022         assert_eq!(
   1023             err,
   1024             RadrootsSp1TradeGuestError::MissingEventEvidence("event_evidence")
   1025         );
   1026     }
   1027 
   1028     #[test]
   1029     fn event_evidence_must_be_preverified() {
   1030         let mut input = witness();
   1031         input.event_evidence[1].preverified_signature = false;
   1032         let err = reduce_order_acceptance_public_values(&input).expect_err("preverified");
   1033         assert_eq!(err, RadrootsSp1TradeGuestError::SignatureNotPreverified);
   1034     }
   1035 
   1036     #[test]
   1037     fn unsupported_event_evidence_kind_is_rejected() {
   1038         let mut input = witness();
   1039         input.event_evidence[1].kind = 1;
   1040         let err = reduce_order_acceptance_public_values(&input).expect_err("kind");
   1041         assert_eq!(
   1042             err,
   1043             RadrootsSp1TradeGuestError::UnsupportedEventEvidenceKind(1)
   1044         );
   1045     }
   1046 
   1047     #[test]
   1048     fn event_evidence_signer_must_match_payload_binding() {
   1049         let mut input = witness();
   1050         input.event_evidence[1].signer_pubkey =
   1051             "3333333333333333333333333333333333333333333333333333333333333333".to_string();
   1052         let err = reduce_order_acceptance_public_values(&input).expect_err("signer");
   1053         assert_eq!(
   1054             err,
   1055             RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch("signer_pubkey")
   1056         );
   1057     }
   1058 
   1059     #[test]
   1060     fn noncanonical_reducer_identity_is_rejected() {
   1061         let mut input = witness();
   1062         input.reducer_program_hash =
   1063             "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1064         let err = reduce_order_acceptance_public_values(&input).expect_err("reducer");
   1065         assert_eq!(
   1066             err,
   1067             RadrootsSp1TradeGuestError::UnsupportedReducerProgramHash
   1068         );
   1069     }
   1070 
   1071     #[test]
   1072     fn noncanonical_protocol_identity_is_rejected() {
   1073         let mut input = witness();
   1074         input.radroots_protocol_version = "radroots.trade.legacy".to_string();
   1075         let err = reduce_order_acceptance_public_values(&input).expect_err("protocol");
   1076         assert_eq!(err, RadrootsSp1TradeGuestError::UnsupportedProtocolVersion);
   1077     }
   1078 
   1079     #[test]
   1080     fn event_evidence_commitment_changes_public_values() {
   1081         let left = reduce_order_acceptance_public_values(&witness()).expect("left");
   1082         let mut input = witness();
   1083         input.event_evidence[1].canonical_event_hash =
   1084             "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1085         let right = reduce_order_acceptance_public_values(&input).expect("right");
   1086         assert_ne!(
   1087             left.public_values.event_set_root,
   1088             right.public_values.event_set_root
   1089         );
   1090         assert_ne!(left.public_values_hash, right.public_values_hash);
   1091 
   1092         let mut input = witness();
   1093         input.event_evidence.reverse();
   1094         let reordered = reduce_order_acceptance_public_values(&input).expect("reordered");
   1095         assert_eq!(
   1096             left.public_values.event_set_root,
   1097             reordered.public_values.event_set_root
   1098         );
   1099         assert_eq!(left.public_values_hash, reordered.public_values_hash);
   1100 
   1101         let mut input = witness();
   1102         input.event_evidence[0].ordering_key = "same".to_string();
   1103         input.event_evidence[1].ordering_key = "same".to_string();
   1104         reduce_order_acceptance_public_values(&input).expect("same ordering key");
   1105     }
   1106 
   1107     #[test]
   1108     fn event_evidence_binding_labels_are_position_specific() {
   1109         let mut input = witness();
   1110         input.event_evidence[1].event_id =
   1111             "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1112         assert_eq!(
   1113             reduce_order_acceptance_public_values(&input).expect_err("order request binding"),
   1114             RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch("order_request")
   1115         );
   1116 
   1117         let mut input = witness();
   1118         input.event_evidence[2].event_id =
   1119             "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1120         assert_eq!(
   1121             reduce_order_acceptance_public_values(&input).expect_err("order decision binding"),
   1122             RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch("order_decision")
   1123         );
   1124     }
   1125 
   1126     #[test]
   1127     fn witness_header_validation_rejects_noncanonical_fields() {
   1128         let mut input = witness();
   1129         input.witness_version = RADROOTS_SP1_TRADE_WITNESS_VERSION + 1;
   1130         assert_eq!(
   1131             reduce_order_acceptance_public_values(&input).expect_err("witness version"),
   1132             RadrootsSp1TradeGuestError::UnsupportedWitnessVersion
   1133         );
   1134 
   1135         let mut input = witness();
   1136         input.proof_target = "trade.order_legacy.v1".to_string();
   1137         assert_eq!(
   1138             reduce_order_acceptance_public_values(&input).expect_err("proof target"),
   1139             RadrootsSp1TradeGuestError::UnsupportedProofTarget
   1140         );
   1141 
   1142         let mut input = witness();
   1143         input.proof_target = " ".to_string();
   1144         assert_eq!(
   1145             reduce_order_acceptance_public_values(&input).expect_err("empty proof target"),
   1146             RadrootsSp1TradeGuestError::EmptyField("proof_target")
   1147         );
   1148 
   1149         let mut input = witness();
   1150         input.listing_event_id = "not-an-event-id".to_string();
   1151         assert_eq!(
   1152             reduce_order_acceptance_public_values(&input).expect_err("listing event id"),
   1153             RadrootsSp1TradeGuestError::InvalidEventId("listing_event_id")
   1154         );
   1155 
   1156         let mut input = witness();
   1157         input.reducer_program_hash = "not-a-hash".to_string();
   1158         assert_eq!(
   1159             reduce_order_acceptance_public_values(&input).expect_err("reducer hash"),
   1160             RadrootsSp1TradeGuestError::InvalidHash("reducer_program_hash")
   1161         );
   1162 
   1163         let mut input = witness();
   1164         input.radroots_protocol_version = String::new();
   1165         assert_eq!(
   1166             reduce_order_acceptance_public_values(&input).expect_err("empty protocol"),
   1167             RadrootsSp1TradeGuestError::EmptyField("radroots_protocol_version")
   1168         );
   1169 
   1170         let mut input = witness();
   1171         input.sp1_program_hash = Some("not-a-hash".to_string());
   1172         assert_eq!(
   1173             reduce_order_acceptance_public_values(&input).expect_err("sp1 program hash"),
   1174             RadrootsSp1TradeGuestError::InvalidHash("sp1_program_hash")
   1175         );
   1176 
   1177         let mut input = witness();
   1178         input.sp1_verifying_key_hash = Some("not-a-hash".to_string());
   1179         assert_eq!(
   1180             reduce_order_acceptance_public_values(&input).expect_err("sp1 verifying key hash"),
   1181             RadrootsSp1TradeGuestError::InvalidHash("sp1_verifying_key_hash")
   1182         );
   1183 
   1184         let mut input = witness();
   1185         input.sp1_program_hash = None;
   1186         input.sp1_verifying_key_hash = None;
   1187         reduce_order_acceptance_public_values(&input).expect("optional sp1 hashes");
   1188     }
   1189 
   1190     #[test]
   1191     fn event_evidence_validation_rejects_shape_and_binding_errors() {
   1192         let mut input = witness();
   1193         input.event_evidence.pop();
   1194         assert_eq!(
   1195             reduce_order_acceptance_public_values(&input).expect_err("evidence len"),
   1196             RadrootsSp1TradeGuestError::InvalidEventEvidence("event_evidence.len")
   1197         );
   1198 
   1199         let mut input = witness();
   1200         input.event_evidence[2].workflow_position =
   1201             RadrootsSp1TradeEventWorkflowPosition::OrderRequest;
   1202         assert_eq!(
   1203             reduce_order_acceptance_public_values(&input).expect_err("duplicate evidence"),
   1204             RadrootsSp1TradeGuestError::DuplicateEventEvidence("order_request")
   1205         );
   1206 
   1207         let mut input = witness();
   1208         input.event_evidence[0].event_id = "not-an-event-id".to_string();
   1209         assert_eq!(
   1210             reduce_order_acceptance_public_values(&input).expect_err("evidence event id"),
   1211             RadrootsSp1TradeGuestError::InvalidEventId("event_evidence.event_id")
   1212         );
   1213 
   1214         let mut input = witness();
   1215         input.event_evidence[0].signer_pubkey = "not-a-pubkey".to_string();
   1216         assert_eq!(
   1217             reduce_order_acceptance_public_values(&input).expect_err("evidence signer"),
   1218             RadrootsSp1TradeGuestError::InvalidEventEvidence("event_evidence.signer_pubkey")
   1219         );
   1220 
   1221         let mut input = witness();
   1222         input.event_evidence[0].canonical_event_hash = "not-a-hash".to_string();
   1223         assert_eq!(
   1224             reduce_order_acceptance_public_values(&input).expect_err("canonical hash"),
   1225             RadrootsSp1TradeGuestError::InvalidHash("event_evidence.canonical_event_hash")
   1226         );
   1227 
   1228         let mut input = witness();
   1229         input.event_evidence[0].signature_hash = "not-a-hash".to_string();
   1230         assert_eq!(
   1231             reduce_order_acceptance_public_values(&input).expect_err("signature hash"),
   1232             RadrootsSp1TradeGuestError::InvalidHash("event_evidence.signature_hash")
   1233         );
   1234 
   1235         let mut input = witness();
   1236         input.event_evidence[0].content_hash = "not-a-hash".to_string();
   1237         assert_eq!(
   1238             reduce_order_acceptance_public_values(&input).expect_err("content hash"),
   1239             RadrootsSp1TradeGuestError::InvalidHash("event_evidence.content_hash")
   1240         );
   1241 
   1242         let mut input = witness();
   1243         input.event_evidence[0].tags_hash = "not-a-hash".to_string();
   1244         assert_eq!(
   1245             reduce_order_acceptance_public_values(&input).expect_err("tags hash"),
   1246             RadrootsSp1TradeGuestError::InvalidHash("event_evidence.tags_hash")
   1247         );
   1248 
   1249         let mut input = witness();
   1250         input.event_evidence[0].ordering_key = " ".to_string();
   1251         assert_eq!(
   1252             reduce_order_acceptance_public_values(&input).expect_err("ordering key"),
   1253             RadrootsSp1TradeGuestError::EmptyField("event_evidence.ordering_key")
   1254         );
   1255 
   1256         let mut input = witness();
   1257         input.event_evidence[0].event_id =
   1258             "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1259         assert_eq!(
   1260             reduce_order_acceptance_public_values(&input).expect_err("listing binding"),
   1261             RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch("listing")
   1262         );
   1263 
   1264         let mut input = witness();
   1265         input.event_evidence[0].role = RadrootsSp1TradeEventEvidenceRole::Buyer;
   1266         assert_eq!(
   1267             reduce_order_acceptance_public_values(&input).expect_err("role binding"),
   1268             RadrootsSp1TradeGuestError::EventEvidenceBindingMismatch("role")
   1269         );
   1270 
   1271         let mut input = witness();
   1272         input.event_evidence[0].kind = RADROOTS_SP1_TRADE_KIND_LISTING_DRAFT;
   1273         reduce_order_acceptance_public_values(&input).expect("listing draft evidence");
   1274     }
   1275 
   1276     #[test]
   1277     fn order_shape_and_binding_validation_rejects_edge_cases() {
   1278         let mut request_case = request(1);
   1279         request_case.items.clear();
   1280         assert_eq!(
   1281             super::validate_order_request_shape(&request_case).expect_err("empty request"),
   1282             RadrootsSp1TradeGuestError::InvalidOrderRequest
   1283         );
   1284 
   1285         let mut request_case = request(1);
   1286         request_case.items[0].bin_id = " ".to_string();
   1287         assert_eq!(
   1288             super::validate_order_request_shape(&request_case).expect_err("empty request bin"),
   1289             RadrootsSp1TradeGuestError::EmptyField("request.items.bin_id")
   1290         );
   1291 
   1292         let mut request_case = request(0);
   1293         assert_eq!(
   1294             super::validate_order_request_shape(&request_case).expect_err("zero request bin count"),
   1295             RadrootsSp1TradeGuestError::InvalidOrderRequest
   1296         );
   1297         request_case.items[0].bin_count = 1;
   1298         super::validate_order_request_shape(&request_case).expect("request shape");
   1299 
   1300         let mut decision_case = decision(1);
   1301         let RadrootsSp1TradeOrderDecisionWitness::Accepted {
   1302             inventory_commitments,
   1303         } = &mut decision_case.decision
   1304         else {
   1305             unreachable!();
   1306         };
   1307         inventory_commitments.clear();
   1308         assert_eq!(
   1309             super::validate_order_decision_shape(&decision_case).expect_err("empty commitments"),
   1310             RadrootsSp1TradeGuestError::InvalidOrderDecision
   1311         );
   1312 
   1313         let mut decision_case = decision(1);
   1314         let RadrootsSp1TradeOrderDecisionWitness::Accepted {
   1315             inventory_commitments,
   1316         } = &mut decision_case.decision
   1317         else {
   1318             unreachable!();
   1319         };
   1320         inventory_commitments[0].bin_id = " ".to_string();
   1321         assert_eq!(
   1322             super::validate_order_decision_shape(&decision_case).expect_err("empty commitment bin"),
   1323             RadrootsSp1TradeGuestError::EmptyField("decision.inventory_commitments.bin_id")
   1324         );
   1325 
   1326         let mut decision_case = decision(0);
   1327         assert_eq!(
   1328             super::validate_order_decision_shape(&decision_case).expect_err("zero commitment"),
   1329             RadrootsSp1TradeGuestError::InvalidOrderDecision
   1330         );
   1331         decision_case.decision = RadrootsSp1TradeOrderDecisionWitness::Declined {
   1332             reason: " ".to_string(),
   1333         };
   1334         assert_eq!(
   1335             super::validate_order_decision_shape(&decision_case).expect_err("empty decline reason"),
   1336             RadrootsSp1TradeGuestError::EmptyField("decision.reason")
   1337         );
   1338         decision_case.decision = RadrootsSp1TradeOrderDecisionWitness::Declined {
   1339             reason: "sold out".to_string(),
   1340         };
   1341         super::validate_order_decision_shape(&decision_case).expect("decline shape");
   1342         assert_eq!(
   1343             super::aggregate_accepted_counts(&decision_case).expect_err("declined aggregate"),
   1344             RadrootsSp1TradeGuestError::DecisionNotAccepted
   1345         );
   1346 
   1347         let mut input = witness();
   1348         input.decision.decision = RadrootsSp1TradeOrderDecisionWitness::Declined {
   1349             reason: "sold out".to_string(),
   1350         };
   1351         assert_eq!(
   1352             reduce_order_acceptance_public_values(&input).expect_err("declined order"),
   1353             RadrootsSp1TradeGuestError::DecisionNotAccepted
   1354         );
   1355 
   1356         let binding_cases: [(&str, fn(&mut RadrootsSp1TradeOrderAcceptanceWitness)); 4] = [
   1357             (
   1358                 "order_id",
   1359                 |input: &mut RadrootsSp1TradeOrderAcceptanceWitness| {
   1360                     input.decision.order_id = "other-order".to_string();
   1361                 },
   1362             ),
   1363             (
   1364                 "listing_addr",
   1365                 |input: &mut RadrootsSp1TradeOrderAcceptanceWitness| {
   1366                     input.decision.listing_addr = "other-listing".to_string();
   1367                 },
   1368             ),
   1369             (
   1370                 "buyer_pubkey",
   1371                 |input: &mut RadrootsSp1TradeOrderAcceptanceWitness| {
   1372                     input.decision.buyer_pubkey =
   1373                         "3333333333333333333333333333333333333333333333333333333333333333"
   1374                             .to_string();
   1375                 },
   1376             ),
   1377             (
   1378                 "seller_pubkey",
   1379                 |input: &mut RadrootsSp1TradeOrderAcceptanceWitness| {
   1380                     input.decision.seller_pubkey =
   1381                         "3333333333333333333333333333333333333333333333333333333333333333"
   1382                             .to_string();
   1383                 },
   1384             ),
   1385         ];
   1386         for (field, apply) in binding_cases {
   1387             let mut input = witness();
   1388             apply(&mut input);
   1389             assert_eq!(
   1390                 reduce_order_acceptance_public_values(&input).expect_err("binding mismatch"),
   1391                 RadrootsSp1TradeGuestError::OrderBindingMismatch(field)
   1392             );
   1393         }
   1394     }
   1395 
   1396     #[test]
   1397     fn inventory_validation_rejects_duplicate_missing_and_overflow() {
   1398         let mut input = witness();
   1399         input.inventory_bins.push(input.inventory_bins[0].clone());
   1400         assert_eq!(
   1401             reduce_order_acceptance_public_values(&input).expect_err("duplicate bin"),
   1402             RadrootsSp1TradeGuestError::DuplicateInventoryBin("bin-1".to_string())
   1403         );
   1404 
   1405         let mut input = witness();
   1406         input.inventory_bins.clear();
   1407         assert_eq!(
   1408             reduce_order_acceptance_public_values(&input).expect_err("missing bin"),
   1409             RadrootsSp1TradeGuestError::MissingInventoryBin("bin-1".to_string())
   1410         );
   1411 
   1412         let mut input = witness();
   1413         input.inventory_bins[0].listing_capacity = u64::MAX;
   1414         input.inventory_bins[0].previous_reserved = u64::MAX;
   1415         assert_eq!(
   1416             reduce_order_acceptance_public_values(&input).expect_err("inventory overflow"),
   1417             RadrootsSp1TradeGuestError::InventoryOverflow
   1418         );
   1419     }
   1420 
   1421     #[test]
   1422     fn public_values_validation_rejects_noncanonical_fields() {
   1423         let execution = reduce_order_acceptance_public_values(&witness()).expect("execution");
   1424 
   1425         let mut optional = execution.public_values.clone();
   1426         optional.sp1_program_hash = None;
   1427         optional.sp1_verifying_key_hash = None;
   1428         optional.listing_addr_hash = None;
   1429         optional.listing_event_id = None;
   1430         optional.order_id_hash = None;
   1431         optional.root_event_id = None;
   1432         optional.target_event_id = None;
   1433         optional.inventory_delta_root = None;
   1434         optional.inventory_sequence = None;
   1435         optional.inventory_prev_root = None;
   1436         optional.inventory_new_root = None;
   1437         canonical_public_values_bytes(&optional).expect("optional public values");
   1438 
   1439         let mut public_values = execution.public_values.clone();
   1440         public_values.schema_version += 1;
   1441         assert_eq!(
   1442             canonical_public_values_bytes(&public_values).expect_err("schema version"),
   1443             RadrootsSp1TradeGuestError::InvalidHash("schema_version")
   1444         );
   1445 
   1446         let mut public_values = execution.public_values.clone();
   1447         public_values.witness_version += 1;
   1448         assert_eq!(
   1449             canonical_public_values_bytes(&public_values).expect_err("witness version"),
   1450             RadrootsSp1TradeGuestError::UnsupportedWitnessVersion
   1451         );
   1452 
   1453         let mut public_values = execution.public_values.clone();
   1454         public_values.proof_target = "legacy".to_string();
   1455         assert_eq!(
   1456             canonical_public_values_bytes(&public_values).expect_err("proof target"),
   1457             RadrootsSp1TradeGuestError::UnsupportedProofTarget
   1458         );
   1459 
   1460         let mut public_values = execution.public_values.clone();
   1461         public_values.radroots_protocol_version = String::new();
   1462         assert_eq!(
   1463             canonical_public_values_bytes(&public_values).expect_err("protocol"),
   1464             RadrootsSp1TradeGuestError::EmptyField("radroots_protocol_version")
   1465         );
   1466 
   1467         let mut public_values = execution.public_values.clone();
   1468         public_values.radroots_protocol_version = "radroots.trade.legacy".to_string();
   1469         assert_eq!(
   1470             canonical_public_values_bytes(&public_values).expect_err("unsupported protocol"),
   1471             RadrootsSp1TradeGuestError::UnsupportedProtocolVersion
   1472         );
   1473 
   1474         let hash_fields: [(&str, fn(&mut super::RadrootsSp1TradeProofPublicValues)); 12] = [
   1475             (
   1476                 "reducer_program_hash",
   1477                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1478                     public_values.reducer_program_hash = "not-a-hash".to_string();
   1479                 },
   1480             ),
   1481             (
   1482                 "sp1_program_hash",
   1483                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1484                     public_values.sp1_program_hash = Some("not-a-hash".to_string());
   1485                 },
   1486             ),
   1487             (
   1488                 "sp1_verifying_key_hash",
   1489                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1490                     public_values.sp1_verifying_key_hash = Some("not-a-hash".to_string());
   1491                 },
   1492             ),
   1493             (
   1494                 "event_set_root",
   1495                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1496                     public_values.event_set_root = "not-a-hash".to_string();
   1497                 },
   1498             ),
   1499             (
   1500                 "listing_addr_hash",
   1501                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1502                     public_values.listing_addr_hash = Some("not-a-hash".to_string());
   1503                 },
   1504             ),
   1505             (
   1506                 "order_id_hash",
   1507                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1508                     public_values.order_id_hash = Some("not-a-hash".to_string());
   1509                 },
   1510             ),
   1511             (
   1512                 "previous_state_root",
   1513                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1514                     public_values.previous_state_root = "not-a-hash".to_string();
   1515                 },
   1516             ),
   1517             (
   1518                 "new_state_root",
   1519                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1520                     public_values.new_state_root = "not-a-hash".to_string();
   1521                 },
   1522             ),
   1523             (
   1524                 "changed_records_root",
   1525                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1526                     public_values.changed_records_root = "not-a-hash".to_string();
   1527                 },
   1528             ),
   1529             (
   1530                 "inventory_delta_root",
   1531                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1532                     public_values.inventory_delta_root = Some("not-a-hash".to_string());
   1533                 },
   1534             ),
   1535             (
   1536                 "inventory_prev_root",
   1537                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1538                     public_values.inventory_prev_root = Some("not-a-hash".to_string());
   1539                 },
   1540             ),
   1541             (
   1542                 "inventory_new_root",
   1543                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1544                     public_values.inventory_new_root = Some("not-a-hash".to_string());
   1545                 },
   1546             ),
   1547         ];
   1548         for (field, apply) in hash_fields {
   1549             let mut public_values = execution.public_values.clone();
   1550             apply(&mut public_values);
   1551             assert_eq!(
   1552                 canonical_public_values_bytes(&public_values).expect_err("hash field"),
   1553                 RadrootsSp1TradeGuestError::InvalidHash(field)
   1554             );
   1555         }
   1556 
   1557         let event_id_fields: [(&str, fn(&mut super::RadrootsSp1TradeProofPublicValues)); 3] = [
   1558             (
   1559                 "listing_event_id",
   1560                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1561                     public_values.listing_event_id = Some("not-an-event-id".to_string());
   1562                 },
   1563             ),
   1564             (
   1565                 "root_event_id",
   1566                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1567                     public_values.root_event_id = Some("not-an-event-id".to_string());
   1568                 },
   1569             ),
   1570             (
   1571                 "target_event_id",
   1572                 |public_values: &mut super::RadrootsSp1TradeProofPublicValues| {
   1573                     public_values.target_event_id = Some("not-an-event-id".to_string());
   1574                 },
   1575             ),
   1576         ];
   1577         for (field, apply) in event_id_fields {
   1578             let mut public_values = execution.public_values.clone();
   1579             apply(&mut public_values);
   1580             assert_eq!(
   1581                 canonical_public_values_bytes(&public_values).expect_err("event id field"),
   1582                 RadrootsSp1TradeGuestError::InvalidEventId(field)
   1583             );
   1584         }
   1585 
   1586         let mut public_values = execution.public_values.clone();
   1587         public_values.error_bitmap = "0x1".to_string();
   1588         assert_eq!(
   1589             canonical_public_values_bytes(&public_values).expect_err("error bitmap"),
   1590             RadrootsSp1TradeGuestError::InvalidHash("error_bitmap")
   1591         );
   1592 
   1593         let mut public_values = execution.public_values;
   1594         public_values.reducer_program_hash =
   1595             "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
   1596         assert_eq!(
   1597             canonical_public_values_bytes(&public_values).expect_err("unsupported reducer"),
   1598             RadrootsSp1TradeGuestError::UnsupportedReducerProgramHash
   1599         );
   1600     }
   1601 
   1602     #[test]
   1603     fn scalar_validators_cover_hex_and_workflow_edges() {
   1604         assert_eq!(
   1605             super::validate_hash32(
   1606                 "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
   1607                 "upper_hash"
   1608             )
   1609             .expect_err("upper hash"),
   1610             RadrootsSp1TradeGuestError::InvalidHash("upper_hash")
   1611         );
   1612         assert_eq!(
   1613             super::validate_hash32(
   1614                 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   1615                 "missing_prefix"
   1616             )
   1617             .expect_err("missing prefix"),
   1618             RadrootsSp1TradeGuestError::InvalidHash("missing_prefix")
   1619         );
   1620         assert_eq!(
   1621             super::validate_hash32(
   1622                 "zzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   1623                 "bad_prefix"
   1624             )
   1625             .expect_err("bad prefix"),
   1626             RadrootsSp1TradeGuestError::InvalidHash("bad_prefix")
   1627         );
   1628         assert_eq!(
   1629             super::validate_event_id(
   1630                 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
   1631                 "upper_event"
   1632             )
   1633             .expect_err("upper event id"),
   1634             RadrootsSp1TradeGuestError::InvalidEventId("upper_event")
   1635         );
   1636         assert_eq!(
   1637             super::validate_hex64(
   1638                 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
   1639                 "upper_hex64"
   1640             )
   1641             .expect_err("upper hex64"),
   1642             RadrootsSp1TradeGuestError::InvalidEventEvidence("upper_hex64")
   1643         );
   1644         assert_eq!(
   1645             RadrootsSp1TradeEventWorkflowPosition::Listing.as_str(),
   1646             "listing"
   1647         );
   1648         assert_eq!(
   1649             RadrootsSp1TradeEventWorkflowPosition::OrderRequest.as_str(),
   1650             "order_request"
   1651         );
   1652         assert_eq!(
   1653             RadrootsSp1TradeEventWorkflowPosition::OrderDecision.as_str(),
   1654             "order_decision"
   1655         );
   1656     }
   1657 }