rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

proof_smoke.rs (23370B)


      1 #![forbid(unsafe_code)]
      2 #![cfg_attr(coverage_nightly, coverage(off))]
      3 
      4 use crate::cli::Command;
      5 use radroots_sp1_guest_trade::{
      6     RADROOTS_SP1_TRADE_KIND_LISTING, RADROOTS_SP1_TRADE_KIND_ORDER_DECISION,
      7     RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET,
      8     RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH,
      9     RADROOTS_SP1_TRADE_WITNESS_VERSION, RadrootsSp1TradeCanonicalEventEvidence,
     10     RadrootsSp1TradeEventEvidenceRole, RadrootsSp1TradeEventWorkflowPosition,
     11     RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeInventoryCommitmentWitness,
     12     RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeOrderDecisionEventWitness,
     13     RadrootsSp1TradeOrderDecisionWitness, RadrootsSp1TradeOrderItemWitness,
     14     RadrootsSp1TradeOrderRequestWitness,
     15 };
     16 use radroots_sp1_host_trade::{
     17     RadrootsSp1TradeProofMode, generate_order_acceptance_proof,
     18     verify_order_acceptance_proof_artifact_structure,
     19 };
     20 use serde::{Deserialize, Serialize};
     21 use std::path::Path;
     22 use std::time::Instant;
     23 use thiserror::Error;
     24 
     25 const PROTOCOL_VERSION: &str = "radroots.rhi.proof_smoke.v0";
     26 const WORKER_NAME: &str = "rhi";
     27 const SP1_VERSION: &str = "6.2.1";
     28 const ORDER_ACCEPTANCE_TINY_FIXTURE: &str = "order_acceptance_tiny_v1";
     29 
     30 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     31 #[serde(deny_unknown_fields)]
     32 pub struct RhiProofSmokeRequest {
     33     pub protocol_version: String,
     34     pub operation: RhiProofSmokeOperation,
     35     pub backend: RhiProofSmokeBackend,
     36     #[serde(default)]
     37     pub fixture: Option<String>,
     38 }
     39 
     40 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     41 #[serde(rename_all = "snake_case")]
     42 pub enum RhiProofSmokeOperation {
     43     Health,
     44     ProofSmoke,
     45 }
     46 
     47 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     48 #[serde(rename_all = "snake_case")]
     49 pub enum RhiProofSmokeBackend {
     50     DeterministicNone,
     51     LocalExecute,
     52 }
     53 
     54 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     55 pub struct RhiProofSmokeResponse {
     56     pub ok: bool,
     57     pub protocol_version: String,
     58     pub operation: RhiProofSmokeOperation,
     59     pub worker_name: String,
     60     pub worker_version: String,
     61     pub git_rev: String,
     62     pub sp1_version: String,
     63     pub backend: RhiProofSmokeBackend,
     64     pub capabilities: Vec<String>,
     65     pub proof_generated: bool,
     66     pub public_values_hash: Option<String>,
     67     pub sp1_program_hash: Option<String>,
     68     pub sp1_verifying_key_hash: Option<String>,
     69     pub event_set_root: Option<String>,
     70     pub reducer_output_root: Option<String>,
     71     pub elapsed_ms: u128,
     72     pub warnings: Vec<String>,
     73     pub error: Option<String>,
     74 }
     75 
     76 #[derive(Debug, Error, PartialEq, Eq)]
     77 pub enum RhiProofSmokeError {
     78     #[error("invalid protocol version")]
     79     InvalidProtocolVersion,
     80     #[error("proof_smoke requires fixture order_acceptance_tiny_v1")]
     81     InvalidFixture,
     82     #[error("local_execute backend is unavailable in this build")]
     83     LocalExecuteUnavailable,
     84     #[error("deterministic proof smoke failed: {0}")]
     85     Deterministic(String),
     86     #[error("SP1 execute proof smoke failed: {0}")]
     87     Sp1Execute(String),
     88 }
     89 
     90 pub async fn run_cli_command(command: Command) -> anyhow::Result<()> {
     91     let Command::ProofSmoke { input, output } = command else {
     92         return Err(anyhow::anyhow!("proof-smoke command expected"));
     93     };
     94     let request_bytes = read_input(input.as_deref())?;
     95     let response = handle_request_bytes(&request_bytes).await;
     96     let response_bytes = serde_json::to_vec_pretty(&response)?;
     97     write_output(output.as_deref(), &response_bytes)?;
     98     if response.ok {
     99         Ok(())
    100     } else {
    101         Err(anyhow::anyhow!(
    102             "{}",
    103             response
    104                 .error
    105                 .as_deref()
    106                 .unwrap_or("proof smoke request failed")
    107         ))
    108     }
    109 }
    110 
    111 pub async fn handle_request_bytes(bytes: &[u8]) -> RhiProofSmokeResponse {
    112     let started = Instant::now();
    113     match serde_json::from_slice::<RhiProofSmokeRequest>(bytes) {
    114         Ok(request) => response_for_request(request, started).await,
    115         Err(error) => response_for_error(
    116             RhiProofSmokeOperation::Health,
    117             RhiProofSmokeBackend::DeterministicNone,
    118             started,
    119             error.to_string(),
    120         ),
    121     }
    122 }
    123 
    124 async fn response_for_request(
    125     request: RhiProofSmokeRequest,
    126     started: Instant,
    127 ) -> RhiProofSmokeResponse {
    128     if request.protocol_version != PROTOCOL_VERSION {
    129         return response_for_error(
    130             request.operation,
    131             request.backend,
    132             started,
    133             RhiProofSmokeError::InvalidProtocolVersion.to_string(),
    134         );
    135     }
    136 
    137     match request.operation {
    138         RhiProofSmokeOperation::Health => response_for_success(
    139             request.operation,
    140             request.backend,
    141             started,
    142             None,
    143             None,
    144             None,
    145             None,
    146             None,
    147             Vec::new(),
    148         ),
    149         RhiProofSmokeOperation::ProofSmoke => {
    150             match run_proof_smoke(request.backend, request.fixture).await {
    151                 Ok(output) => response_for_success(
    152                     request.operation,
    153                     request.backend,
    154                     started,
    155                     Some(output.public_values_hash),
    156                     output.sp1_program_hash,
    157                     output.sp1_verifying_key_hash,
    158                     Some(output.event_set_root),
    159                     Some(output.reducer_output_root),
    160                     output.warnings,
    161                 ),
    162                 Err(error) => response_for_error(
    163                     request.operation,
    164                     request.backend,
    165                     started,
    166                     error.to_string(),
    167                 ),
    168             }
    169         }
    170     }
    171 }
    172 
    173 struct RhiProofSmokeOutput {
    174     public_values_hash: String,
    175     sp1_program_hash: Option<String>,
    176     sp1_verifying_key_hash: Option<String>,
    177     event_set_root: String,
    178     reducer_output_root: String,
    179     warnings: Vec<String>,
    180 }
    181 
    182 async fn run_proof_smoke(
    183     backend: RhiProofSmokeBackend,
    184     fixture: Option<String>,
    185 ) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> {
    186     if fixture.as_deref() != Some(ORDER_ACCEPTANCE_TINY_FIXTURE) {
    187         return Err(RhiProofSmokeError::InvalidFixture);
    188     }
    189 
    190     let witness = order_acceptance_tiny_witness();
    191     match backend {
    192         RhiProofSmokeBackend::DeterministicNone => deterministic_smoke(&witness),
    193         RhiProofSmokeBackend::LocalExecute => local_execute_smoke(&witness).await,
    194     }
    195 }
    196 
    197 fn deterministic_smoke(
    198     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    199 ) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> {
    200     let bundle = generate_order_acceptance_proof(witness, RadrootsSp1TradeProofMode::None)
    201         .map_err(|error| RhiProofSmokeError::Deterministic(error.to_string()))?;
    202     verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof)
    203         .map_err(|error| RhiProofSmokeError::Deterministic(error.to_string()))?;
    204     Ok(RhiProofSmokeOutput {
    205         public_values_hash: canonical_hex_64(&bundle.execution.public_values_hash)?,
    206         sp1_program_hash: None,
    207         sp1_verifying_key_hash: None,
    208         event_set_root: canonical_hex_64(&bundle.execution.public_values.event_set_root)?,
    209         reducer_output_root: canonical_hex_64(&bundle.execution.public_values.new_state_root)?,
    210         warnings: Vec::new(),
    211     })
    212 }
    213 
    214 #[cfg(feature = "sp1_proving")]
    215 async fn local_execute_smoke(
    216     witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    217 ) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> {
    218     let execution = radroots_sp1_host_trade::execute_order_acceptance_sp1_public_values(witness)
    219         .await
    220         .map_err(|error| RhiProofSmokeError::Sp1Execute(error.to_string()))?
    221         .execution;
    222     Ok(RhiProofSmokeOutput {
    223         public_values_hash: canonical_hex_64(&execution.public_values_hash)?,
    224         sp1_program_hash: execution
    225             .public_values
    226             .sp1_program_hash
    227             .as_deref()
    228             .map(canonical_hex_64)
    229             .transpose()?,
    230         sp1_verifying_key_hash: execution
    231             .public_values
    232             .sp1_verifying_key_hash
    233             .as_deref()
    234             .map(canonical_hex_64)
    235             .transpose()?,
    236         event_set_root: canonical_hex_64(&execution.public_values.event_set_root)?,
    237         reducer_output_root: canonical_hex_64(&execution.public_values.new_state_root)?,
    238         warnings: Vec::new(),
    239     })
    240 }
    241 
    242 #[cfg(not(feature = "sp1_proving"))]
    243 async fn local_execute_smoke(
    244     _witness: &RadrootsSp1TradeOrderAcceptanceWitness,
    245 ) -> Result<RhiProofSmokeOutput, RhiProofSmokeError> {
    246     Err(RhiProofSmokeError::LocalExecuteUnavailable)
    247 }
    248 
    249 fn response_for_success(
    250     operation: RhiProofSmokeOperation,
    251     backend: RhiProofSmokeBackend,
    252     started: Instant,
    253     public_values_hash: Option<String>,
    254     sp1_program_hash: Option<String>,
    255     sp1_verifying_key_hash: Option<String>,
    256     event_set_root: Option<String>,
    257     reducer_output_root: Option<String>,
    258     warnings: Vec<String>,
    259 ) -> RhiProofSmokeResponse {
    260     RhiProofSmokeResponse {
    261         ok: true,
    262         protocol_version: PROTOCOL_VERSION.to_string(),
    263         operation,
    264         worker_name: WORKER_NAME.to_string(),
    265         worker_version: env!("CARGO_PKG_VERSION").to_string(),
    266         git_rev: option_env!("RADROOTS_GIT_REV")
    267             .unwrap_or("unknown")
    268             .to_string(),
    269         sp1_version: SP1_VERSION.to_string(),
    270         backend,
    271         capabilities: capabilities(),
    272         proof_generated: false,
    273         public_values_hash,
    274         sp1_program_hash,
    275         sp1_verifying_key_hash,
    276         event_set_root,
    277         reducer_output_root,
    278         elapsed_ms: started.elapsed().as_millis(),
    279         warnings,
    280         error: None,
    281     }
    282 }
    283 
    284 fn canonical_hex_64(value: &str) -> Result<String, RhiProofSmokeError> {
    285     let candidate = value.strip_prefix("0x").unwrap_or(value);
    286     if candidate.len() == 64 && candidate.bytes().all(|byte| byte.is_ascii_hexdigit()) {
    287         return Ok(candidate.to_ascii_lowercase());
    288     }
    289     Err(RhiProofSmokeError::Deterministic(
    290         "public value is not canonical 32-byte hex".to_string(),
    291     ))
    292 }
    293 
    294 fn response_for_error(
    295     operation: RhiProofSmokeOperation,
    296     backend: RhiProofSmokeBackend,
    297     started: Instant,
    298     error: String,
    299 ) -> RhiProofSmokeResponse {
    300     let mut response = response_for_success(
    301         operation,
    302         backend,
    303         started,
    304         None,
    305         None,
    306         None,
    307         None,
    308         None,
    309         Vec::new(),
    310     );
    311     response.ok = false;
    312     response.error = Some(error);
    313     response
    314 }
    315 
    316 fn capabilities() -> Vec<String> {
    317     let mut values = vec![
    318         "health".to_string(),
    319         "proof_smoke".to_string(),
    320         "deterministic_none".to_string(),
    321     ];
    322     if cfg!(feature = "sp1_proving") {
    323         values.push("local_execute".to_string());
    324     }
    325     values
    326 }
    327 
    328 pub(crate) fn order_acceptance_tiny_witness() -> RadrootsSp1TradeOrderAcceptanceWitness {
    329     RadrootsSp1TradeOrderAcceptanceWitness {
    330         witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION,
    331         proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(),
    332         listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    333             .to_string(),
    334         request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    335             .to_string(),
    336         decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    337             .to_string(),
    338         event_evidence: order_acceptance_tiny_event_evidence(),
    339         request: RadrootsSp1TradeOrderRequestWitness {
    340             order_id: "order-1".to_string(),
    341             listing_addr:
    342                 "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1"
    343                     .to_string(),
    344             buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    345                 .to_string(),
    346             seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    347                 .to_string(),
    348             items: vec![RadrootsSp1TradeOrderItemWitness {
    349                 bin_id: "bin-1".to_string(),
    350                 bin_count: 2,
    351             }],
    352         },
    353         decision: RadrootsSp1TradeOrderDecisionEventWitness {
    354             order_id: "order-1".to_string(),
    355             listing_addr:
    356                 "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1"
    357                     .to_string(),
    358             buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    359                 .to_string(),
    360             seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    361                 .to_string(),
    362             decision: RadrootsSp1TradeOrderDecisionWitness::Accepted {
    363                 inventory_commitments: vec![RadrootsSp1TradeInventoryCommitmentWitness {
    364                     bin_id: "bin-1".to_string(),
    365                     bin_count: 2,
    366                 }],
    367             },
    368         },
    369         inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness {
    370             bin_id: "bin-1".to_string(),
    371             listing_capacity: 5,
    372             previous_reserved: 1,
    373         }],
    374         inventory_sequence: 7,
    375         previous_state_root: None,
    376         reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(),
    377         radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(),
    378         sp1_program_hash: None,
    379         sp1_verifying_key_hash: None,
    380     }
    381 }
    382 
    383 fn order_acceptance_tiny_event_evidence() -> Vec<RadrootsSp1TradeCanonicalEventEvidence> {
    384     vec![
    385         RadrootsSp1TradeCanonicalEventEvidence {
    386             event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    387                 .to_string(),
    388             signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    389                 .to_string(),
    390             kind: RADROOTS_SP1_TRADE_KIND_LISTING,
    391             canonical_event_hash:
    392                 "0x1010101010101010101010101010101010101010101010101010101010101010".to_string(),
    393             signature_hash: "0x1111111111111111111111111111111111111111111111111111111111111111"
    394                 .to_string(),
    395             preverified_signature: true,
    396             role: RadrootsSp1TradeEventEvidenceRole::Seller,
    397             workflow_position: RadrootsSp1TradeEventWorkflowPosition::Listing,
    398             content_hash: "0x1212121212121212121212121212121212121212121212121212121212121212"
    399                 .to_string(),
    400             tags_hash: "0x1313131313131313131313131313131313131313131313131313131313131313"
    401                 .to_string(),
    402             ordering_key: "001:listing".to_string(),
    403         },
    404         RadrootsSp1TradeCanonicalEventEvidence {
    405             event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    406                 .to_string(),
    407             signer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222"
    408                 .to_string(),
    409             kind: RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST,
    410             canonical_event_hash:
    411                 "0x2020202020202020202020202020202020202020202020202020202020202020".to_string(),
    412             signature_hash: "0x2121212121212121212121212121212121212121212121212121212121212121"
    413                 .to_string(),
    414             preverified_signature: true,
    415             role: RadrootsSp1TradeEventEvidenceRole::Buyer,
    416             workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderRequest,
    417             content_hash: "0x2222222222222222222222222222222222222222222222222222222222222222"
    418                 .to_string(),
    419             tags_hash: "0x2323232323232323232323232323232323232323232323232323232323232323"
    420                 .to_string(),
    421             ordering_key: "002:order_request".to_string(),
    422         },
    423         RadrootsSp1TradeCanonicalEventEvidence {
    424             event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    425                 .to_string(),
    426             signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111"
    427                 .to_string(),
    428             kind: RADROOTS_SP1_TRADE_KIND_ORDER_DECISION,
    429             canonical_event_hash:
    430                 "0x3030303030303030303030303030303030303030303030303030303030303030".to_string(),
    431             signature_hash: "0x3131313131313131313131313131313131313131313131313131313131313131"
    432                 .to_string(),
    433             preverified_signature: true,
    434             role: RadrootsSp1TradeEventEvidenceRole::Seller,
    435             workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderDecision,
    436             content_hash: "0x3232323232323232323232323232323232323232323232323232323232323232"
    437                 .to_string(),
    438             tags_hash: "0x3333333333333333333333333333333333333333333333333333333333333333"
    439                 .to_string(),
    440             ordering_key: "003:order_decision".to_string(),
    441         },
    442     ]
    443 }
    444 
    445 fn read_input(input: Option<&Path>) -> anyhow::Result<Vec<u8>> {
    446     match input {
    447         Some(path) => Ok(std::fs::read(path)?),
    448         None => {
    449             use std::io::Read;
    450             let mut bytes = Vec::new();
    451             std::io::stdin().read_to_end(&mut bytes)?;
    452             Ok(bytes)
    453         }
    454     }
    455 }
    456 
    457 fn write_output(output: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> {
    458     match output {
    459         Some(path) => {
    460             std::fs::write(path, bytes)?;
    461             Ok(())
    462         }
    463         None => {
    464             println!("{}", String::from_utf8_lossy(bytes));
    465             Ok(())
    466         }
    467     }
    468 }
    469 
    470 #[cfg(test)]
    471 mod tests {
    472     use super::{
    473         PROTOCOL_VERSION, RhiProofSmokeBackend, RhiProofSmokeOperation, RhiProofSmokeRequest,
    474         RhiProofSmokeResponse, handle_request_bytes,
    475     };
    476 
    477     fn request(operation: RhiProofSmokeOperation, backend: RhiProofSmokeBackend) -> Vec<u8> {
    478         serde_json::to_vec(&RhiProofSmokeRequest {
    479             protocol_version: PROTOCOL_VERSION.to_string(),
    480             operation,
    481             backend,
    482             fixture: Some("order_acceptance_tiny_v1".to_string()),
    483         })
    484         .expect("request json")
    485     }
    486 
    487     #[tokio::test]
    488     async fn health_returns_worker_capabilities() {
    489         let bytes = serde_json::to_vec(&RhiProofSmokeRequest {
    490             protocol_version: PROTOCOL_VERSION.to_string(),
    491             operation: RhiProofSmokeOperation::Health,
    492             backend: RhiProofSmokeBackend::DeterministicNone,
    493             fixture: None,
    494         })
    495         .expect("request json");
    496         let response: RhiProofSmokeResponse = handle_request_bytes(&bytes).await;
    497         assert!(response.ok);
    498         assert_eq!(response.worker_name, "rhi");
    499         assert!(response.capabilities.contains(&"health".to_string()));
    500         assert!(!response.proof_generated);
    501     }
    502 
    503     #[tokio::test]
    504     async fn deterministic_proof_smoke_returns_public_values() {
    505         let response = handle_request_bytes(&request(
    506             RhiProofSmokeOperation::ProofSmoke,
    507             RhiProofSmokeBackend::DeterministicNone,
    508         ))
    509         .await;
    510         assert!(response.ok);
    511         assert_eq!(response.operation, RhiProofSmokeOperation::ProofSmoke);
    512         assert!(response.public_values_hash.is_some());
    513         assert!(response.sp1_program_hash.is_none());
    514         assert!(response.sp1_verifying_key_hash.is_none());
    515         assert!(response.event_set_root.is_some());
    516         assert!(response.reducer_output_root.is_some());
    517         for value in [
    518             response.public_values_hash.as_deref(),
    519             response.event_set_root.as_deref(),
    520             response.reducer_output_root.as_deref(),
    521         ] {
    522             let value = value.expect("public value");
    523             assert_eq!(value.len(), 64);
    524             assert!(!value.starts_with("0x"));
    525             assert!(value.bytes().all(|byte| byte.is_ascii_hexdigit()));
    526         }
    527         assert!(!response.proof_generated);
    528     }
    529 
    530     #[tokio::test]
    531     async fn proof_smoke_rejects_unknown_fixture() {
    532         let response = handle_request_bytes(
    533             &serde_json::to_vec(&RhiProofSmokeRequest {
    534                 protocol_version: PROTOCOL_VERSION.to_string(),
    535                 operation: RhiProofSmokeOperation::ProofSmoke,
    536                 backend: RhiProofSmokeBackend::DeterministicNone,
    537                 fixture: Some("other".to_string()),
    538             })
    539             .expect("request json"),
    540         )
    541         .await;
    542         assert!(!response.ok);
    543         assert_eq!(
    544             response.error.as_deref(),
    545             Some("proof_smoke requires fixture order_acceptance_tiny_v1")
    546         );
    547     }
    548 
    549     #[tokio::test]
    550     async fn proof_smoke_rejects_full_proof_request_fields() {
    551         let response = handle_request_bytes(
    552             br#"{"protocol_version":"radroots.rhi.proof_smoke.v0","operation":"proof_smoke","backend":"deterministic_none","fixture":"order_acceptance_tiny_v1","proof_mode":"core"}"#,
    553         )
    554         .await;
    555         assert!(!response.ok);
    556         assert!(
    557             response
    558                 .error
    559                 .as_deref()
    560                 .is_some_and(|error| error.contains("unknown field"))
    561         );
    562     }
    563 
    564     #[cfg(not(feature = "sp1_proving"))]
    565     #[tokio::test]
    566     async fn local_execute_reports_unavailable_without_feature() {
    567         let response = handle_request_bytes(&request(
    568             RhiProofSmokeOperation::ProofSmoke,
    569             RhiProofSmokeBackend::LocalExecute,
    570         ))
    571         .await;
    572         assert!(!response.ok);
    573         assert_eq!(
    574             response.error.as_deref(),
    575             Some("local_execute backend is unavailable in this build")
    576         );
    577     }
    578 
    579     #[cfg(feature = "sp1_proving")]
    580     #[tokio::test]
    581     async fn local_execute_returns_sp1_public_values_without_proof_generation() {
    582         let deterministic = handle_request_bytes(&request(
    583             RhiProofSmokeOperation::ProofSmoke,
    584             RhiProofSmokeBackend::DeterministicNone,
    585         ))
    586         .await;
    587         let response = handle_request_bytes(&request(
    588             RhiProofSmokeOperation::ProofSmoke,
    589             RhiProofSmokeBackend::LocalExecute,
    590         ))
    591         .await;
    592         assert!(response.ok);
    593         assert_eq!(response.operation, RhiProofSmokeOperation::ProofSmoke);
    594         assert_eq!(response.backend, RhiProofSmokeBackend::LocalExecute);
    595         assert!(response.capabilities.contains(&"local_execute".to_string()));
    596         assert!(!response.proof_generated);
    597         assert_ne!(
    598             response.public_values_hash,
    599             deterministic.public_values_hash
    600         );
    601         assert!(response.sp1_program_hash.is_some());
    602         assert!(response.sp1_verifying_key_hash.is_some());
    603         for value in [
    604             response.sp1_program_hash.as_deref(),
    605             response.sp1_verifying_key_hash.as_deref(),
    606         ] {
    607             let value = value.expect("SP1 identity");
    608             assert_eq!(value.len(), 64);
    609             assert!(!value.starts_with("0x"));
    610             assert!(value.bytes().all(|byte| byte.is_ascii_hexdigit()));
    611         }
    612         assert_eq!(response.event_set_root, deterministic.event_set_root);
    613         assert_eq!(
    614             response.reducer_output_root,
    615             deterministic.reducer_output_root
    616         );
    617         assert!(response.error.is_none());
    618     }
    619 }