lib

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

commit 8a71c0093391976e75e71a542959386d1b6e1294
parent 1919e437235f6e8dd19e10df452debf8f7a7d83a
Author: triesap <tyson@radroots.org>
Date:   Sun, 17 May 2026 00:26:48 +0000

sp1 trade proof foundation

Diffstat:
MCargo.lock | 28++++++++++++++++++++++++++++
MCargo.toml | 4++++
Acrates/sp1_guest_trade/Cargo.toml | 28++++++++++++++++++++++++++++
Acrates/sp1_guest_trade/src/lib.rs | 654+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sp1_host_trade/Cargo.toml | 31+++++++++++++++++++++++++++++++
Acrates/sp1_host_trade/src/lib.rs | 480+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mspec/manifest.toml | 2++
7 files changed, 1227 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2766,6 +2766,34 @@ dependencies = [ ] [[package]] +name = "radroots_sp1_guest_trade" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_core", + "radroots_events", + "radroots_trade", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "radroots_sp1_host_trade" +version = "0.1.0-alpha.2" +dependencies = [ + "base64 0.22.1", + "radroots_core", + "radroots_events", + "radroots_sp1_guest_trade", + "radroots_trade", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", +] + +[[package]] name = "radroots_sql_core" version = "0.1.0-alpha.2" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -38,6 +38,8 @@ members = [ "crates/runtime_distribution", "crates/runtime_manager", "crates/sdk", + "crates/sp1_guest_trade", + "crates/sp1_host_trade", "crates/trade", "crates/types", "crates/protected_store", @@ -95,6 +97,8 @@ radroots_trade = { path = "crates/trade", version = "0.1.0-alpha.2", default-fea radroots_types = { path = "crates/types", version = "0.1.0-alpha.2", default-features = false } radroots_protected_store = { path = "crates/protected_store", version = "0.1.0-alpha.2", default-features = false } radroots_secret_vault = { path = "crates/secret_vault", version = "0.1.0-alpha.2", default-features = false } +radroots_sp1_guest_trade = { path = "crates/sp1_guest_trade", version = "0.1.0-alpha.2", default-features = false } +radroots_sp1_host_trade = { path = "crates/sp1_host_trade", version = "0.1.0-alpha.2", default-features = false } anyhow = { version = "1" } base64 = { version = "0.22" } diff --git a/crates/sp1_guest_trade/Cargo.toml b/crates/sp1_guest_trade/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "radroots_sp1_guest_trade" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Deterministic Radroots trade public-values reducer for SP1 guests" +repository.workspace = true +homepage.workspace = true + +[dependencies] +radroots_events = { workspace = true, default-features = false, features = [ + "serde", + "std", +] } +radroots_trade = { workspace = true, default-features = false, features = [ + "serde_json", + "std", +] } +serde = { workspace = true, default-features = false, features = ["alloc", "derive"] } +serde_json = { workspace = true, default-features = false, features = ["alloc"] } +sha2 = { workspace = true, default-features = false } +thiserror = { workspace = true } + +[dev-dependencies] +radroots_core = { workspace = true } diff --git a/crates/sp1_guest_trade/src/lib.rs b/crates/sp1_guest_trade/src/lib.rs @@ -0,0 +1,654 @@ +#![forbid(unsafe_code)] + +use radroots_events::trade::{ + RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested, +}; +use radroots_trade::validation_receipt::validation_receipt_public_values_hash_hex; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; +use thiserror::Error; + +pub const RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION: u32 = 1; +pub const RADROOTS_SP1_TRADE_PROTOCOL_VERSION: &str = "radroots.trade.v1"; +pub const RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH: &str = + "0x3d8f7f463904d71f2d0d14b1551450756697e51c7b658e10c6d5c20a7bc61f08"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofStatementType { + TradeTransition, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofTransitionKind { + OrderAccepted, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofResult { + Valid, + Invalid, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeProofPublicValues { + pub schema_version: u32, + pub statement_type: RadrootsSp1TradeProofStatementType, + pub radroots_protocol_version: String, + pub reducer_program_hash: String, + pub sp1_verifying_key_hash: Option<String>, + pub event_set_root: String, + pub listing_addr_hash: Option<String>, + pub listing_event_id: Option<String>, + pub order_id_hash: Option<String>, + pub root_event_id: Option<String>, + pub target_event_id: Option<String>, + pub previous_state_root: String, + pub new_state_root: String, + pub transition: Option<RadrootsSp1TradeProofTransitionKind>, + pub result: RadrootsSp1TradeProofResult, + pub error_bitmap: String, + pub inventory_delta_root: Option<String>, + pub inventory_sequence: Option<u128>, + pub inventory_prev_root: Option<String>, + pub inventory_new_root: Option<String>, + pub changed_records_root: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeInventoryBinWitness { + pub bin_id: String, + pub listing_capacity: u64, + pub previous_reserved: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeOrderAcceptanceWitness { + pub listing_event_id: String, + pub request_event_id: String, + pub decision_event_id: String, + pub request: RadrootsTradeOrderRequested, + pub decision: RadrootsTradeOrderDecisionEvent, + pub inventory_bins: Vec<RadrootsSp1TradeInventoryBinWitness>, + pub inventory_sequence: u128, + pub previous_state_root: Option<String>, + pub reducer_program_hash: String, + pub radroots_protocol_version: String, + pub sp1_verifying_key_hash: Option<String>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradePublicValuesExecution { + pub public_values: RadrootsSp1TradeProofPublicValues, + pub canonical_public_values: Vec<u8>, + pub public_values_hash: String, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RadrootsSp1TradeGuestError { + #[error("{0} cannot be empty")] + EmptyField(&'static str), + #[error("invalid event id field {0}")] + InvalidEventId(&'static str), + #[error("invalid hash field {0}")] + InvalidHash(&'static str), + #[error("invalid order request")] + InvalidOrderRequest, + #[error("invalid order decision")] + InvalidOrderDecision, + #[error("order decision is not accepted")] + DecisionNotAccepted, + #[error("order field {0} does not match")] + OrderBindingMismatch(&'static str), + #[error("inventory bin {0} is missing")] + MissingInventoryBin(String), + #[error("inventory bin {0} is duplicated")] + DuplicateInventoryBin(String), + #[error("inventory commitment does not match order request")] + InventoryCommitmentMismatch, + #[error("inventory bin {0} would overcommit listing capacity")] + InventoryOvercommit(String), + #[error("inventory quantity overflow")] + InventoryOverflow, + #[error("public values encoding failed")] + PublicValuesEncoding, +} + +pub fn reduce_order_acceptance_public_values( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradePublicValuesExecution, RadrootsSp1TradeGuestError> { + validate_witness_header(witness)?; + witness + .request + .validate() + .map_err(|_| RadrootsSp1TradeGuestError::InvalidOrderRequest)?; + witness + .decision + .validate() + .map_err(|_| RadrootsSp1TradeGuestError::InvalidOrderDecision)?; + validate_order_binding(witness)?; + + let request_counts = aggregate_requested_counts(&witness.request)?; + let accepted_counts = aggregate_accepted_counts(&witness.decision)?; + if request_counts != accepted_counts { + return Err(RadrootsSp1TradeGuestError::InventoryCommitmentMismatch); + } + + let inventory_bins = inventory_bins_by_id(&witness.inventory_bins)?; + let next_inventory = apply_inventory_delta(&request_counts, &inventory_bins)?; + let previous_state_root = witness + .previous_state_root + .clone() + .unwrap_or_else(empty_state_root); + validate_hash32(&previous_state_root, "previous_state_root")?; + + let event_set_root = event_set_root([ + witness.listing_event_id.as_str(), + witness.request_event_id.as_str(), + witness.decision_event_id.as_str(), + ]); + let inventory_delta_root = hash_json("radroots:inventory-delta:v1", &request_counts)?; + let inventory_prev_root = hash_json("radroots:inventory-prev:v1", &inventory_bins)?; + let inventory_new_root = hash_json("radroots:inventory-new:v1", &next_inventory)?; + let changed_records_root = hash_json( + "radroots:changed-records:v1", + &ChangedRecordsMaterial { + order_id: &witness.request.order_id, + listing_addr: &witness.request.listing_addr, + target_event_id: &witness.decision_event_id, + inventory_new_root: &inventory_new_root, + }, + )?; + let new_state_root = hash_json( + "radroots:state-root:v1", + &StateRootMaterial { + previous_state_root: &previous_state_root, + event_set_root: &event_set_root, + changed_records_root: &changed_records_root, + inventory_new_root: &inventory_new_root, + }, + )?; + + let public_values = RadrootsSp1TradeProofPublicValues { + schema_version: RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION, + statement_type: RadrootsSp1TradeProofStatementType::TradeTransition, + radroots_protocol_version: witness.radroots_protocol_version.clone(), + reducer_program_hash: witness.reducer_program_hash.clone(), + sp1_verifying_key_hash: witness.sp1_verifying_key_hash.clone(), + event_set_root, + listing_addr_hash: Some(hash_bytes( + "radroots:listing-addr:v1", + witness.request.listing_addr.as_bytes(), + )), + listing_event_id: Some(witness.listing_event_id.clone()), + order_id_hash: Some(hash_bytes( + "radroots:order-id:v1", + witness.request.order_id.as_bytes(), + )), + root_event_id: Some(witness.request_event_id.clone()), + target_event_id: Some(witness.decision_event_id.clone()), + previous_state_root, + new_state_root, + transition: Some(RadrootsSp1TradeProofTransitionKind::OrderAccepted), + result: RadrootsSp1TradeProofResult::Valid, + error_bitmap: zero_error_bitmap().to_string(), + inventory_delta_root: Some(inventory_delta_root), + inventory_sequence: Some(witness.inventory_sequence), + inventory_prev_root: Some(inventory_prev_root), + inventory_new_root: Some(inventory_new_root), + changed_records_root, + }; + let canonical_public_values = canonical_public_values_bytes(&public_values)?; + let public_values_hash = validation_receipt_public_values_hash_hex(&canonical_public_values); + Ok(RadrootsSp1TradePublicValuesExecution { + public_values, + canonical_public_values, + public_values_hash, + }) +} + +pub fn canonical_public_values_bytes( + public_values: &RadrootsSp1TradeProofPublicValues, +) -> Result<Vec<u8>, RadrootsSp1TradeGuestError> { + validate_public_values(public_values)?; + serde_json::to_vec(public_values).map_err(|_| RadrootsSp1TradeGuestError::PublicValuesEncoding) +} + +pub fn public_values_hash_hex( + public_values: &RadrootsSp1TradeProofPublicValues, +) -> Result<String, RadrootsSp1TradeGuestError> { + let bytes = canonical_public_values_bytes(public_values)?; + Ok(validation_receipt_public_values_hash_hex(&bytes)) +} + +pub fn empty_state_root() -> String { + hash_bytes("radroots:state-empty:v1", &[]) +} + +fn validate_witness_header( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<(), RadrootsSp1TradeGuestError> { + validate_event_id(&witness.listing_event_id, "listing_event_id")?; + validate_event_id(&witness.request_event_id, "request_event_id")?; + validate_event_id(&witness.decision_event_id, "decision_event_id")?; + validate_required_str(&witness.reducer_program_hash, "reducer_program_hash")?; + validate_hash32(&witness.reducer_program_hash, "reducer_program_hash")?; + validate_required_str( + &witness.radroots_protocol_version, + "radroots_protocol_version", + )?; + if let Some(hash) = &witness.sp1_verifying_key_hash { + validate_hash32(hash, "sp1_verifying_key_hash")?; + } + Ok(()) +} + +fn validate_order_binding( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<(), RadrootsSp1TradeGuestError> { + if !matches!( + witness.decision.decision, + RadrootsTradeOrderDecision::Accepted { .. } + ) { + return Err(RadrootsSp1TradeGuestError::DecisionNotAccepted); + } + if witness.request.order_id != witness.decision.order_id { + return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch("order_id")); + } + if witness.request.listing_addr != witness.decision.listing_addr { + return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch( + "listing_addr", + )); + } + if witness.request.buyer_pubkey != witness.decision.buyer_pubkey { + return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch( + "buyer_pubkey", + )); + } + if witness.request.seller_pubkey != witness.decision.seller_pubkey { + return Err(RadrootsSp1TradeGuestError::OrderBindingMismatch( + "seller_pubkey", + )); + } + Ok(()) +} + +fn aggregate_requested_counts( + request: &RadrootsTradeOrderRequested, +) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> { + let mut counts = BTreeMap::new(); + for item in &request.items { + let entry = counts.entry(item.bin_id.clone()).or_insert(0u64); + *entry = entry + .checked_add(u64::from(item.bin_count)) + .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?; + } + Ok(counts) +} + +fn aggregate_accepted_counts( + decision: &RadrootsTradeOrderDecisionEvent, +) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> { + let RadrootsTradeOrderDecision::Accepted { + inventory_commitments, + } = &decision.decision + else { + return Err(RadrootsSp1TradeGuestError::DecisionNotAccepted); + }; + let mut counts = BTreeMap::new(); + for commitment in inventory_commitments { + let entry = counts.entry(commitment.bin_id.clone()).or_insert(0u64); + *entry = entry + .checked_add(u64::from(commitment.bin_count)) + .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?; + } + Ok(counts) +} + +fn inventory_bins_by_id( + bins: &[RadrootsSp1TradeInventoryBinWitness], +) -> Result<BTreeMap<String, RadrootsSp1TradeInventoryBinWitness>, RadrootsSp1TradeGuestError> { + let mut result = BTreeMap::new(); + for bin in bins { + validate_required_str(&bin.bin_id, "inventory_bins.bin_id")?; + if result.insert(bin.bin_id.clone(), bin.clone()).is_some() { + return Err(RadrootsSp1TradeGuestError::DuplicateInventoryBin( + bin.bin_id.clone(), + )); + } + } + Ok(result) +} + +fn apply_inventory_delta( + request_counts: &BTreeMap<String, u64>, + bins: &BTreeMap<String, RadrootsSp1TradeInventoryBinWitness>, +) -> Result<BTreeMap<String, u64>, RadrootsSp1TradeGuestError> { + let mut next = BTreeMap::new(); + for (bin_id, requested) in request_counts { + let bin = bins + .get(bin_id) + .ok_or_else(|| RadrootsSp1TradeGuestError::MissingInventoryBin(bin_id.clone()))?; + let reserved = bin + .previous_reserved + .checked_add(*requested) + .ok_or(RadrootsSp1TradeGuestError::InventoryOverflow)?; + if reserved > bin.listing_capacity { + return Err(RadrootsSp1TradeGuestError::InventoryOvercommit( + bin_id.clone(), + )); + } + next.insert(bin_id.clone(), reserved); + } + Ok(next) +} + +fn validate_public_values( + public_values: &RadrootsSp1TradeProofPublicValues, +) -> Result<(), RadrootsSp1TradeGuestError> { + if public_values.schema_version != RADROOTS_SP1_TRADE_PUBLIC_VALUES_SCHEMA_VERSION { + return Err(RadrootsSp1TradeGuestError::InvalidHash("schema_version")); + } + validate_required_str( + &public_values.radroots_protocol_version, + "radroots_protocol_version", + )?; + validate_hash32(&public_values.reducer_program_hash, "reducer_program_hash")?; + if let Some(hash) = &public_values.sp1_verifying_key_hash { + validate_hash32(hash, "sp1_verifying_key_hash")?; + } + validate_hash32(&public_values.event_set_root, "event_set_root")?; + if let Some(hash) = &public_values.listing_addr_hash { + validate_hash32(hash, "listing_addr_hash")?; + } + if let Some(event_id) = &public_values.listing_event_id { + validate_event_id(event_id, "listing_event_id")?; + } + if let Some(hash) = &public_values.order_id_hash { + validate_hash32(hash, "order_id_hash")?; + } + if let Some(event_id) = &public_values.root_event_id { + validate_event_id(event_id, "root_event_id")?; + } + if let Some(event_id) = &public_values.target_event_id { + validate_event_id(event_id, "target_event_id")?; + } + validate_hash32(&public_values.previous_state_root, "previous_state_root")?; + validate_hash32(&public_values.new_state_root, "new_state_root")?; + validate_hash32(&public_values.changed_records_root, "changed_records_root")?; + if public_values.error_bitmap != zero_error_bitmap() { + return Err(RadrootsSp1TradeGuestError::InvalidHash("error_bitmap")); + } + if let Some(hash) = &public_values.inventory_delta_root { + validate_hash32(hash, "inventory_delta_root")?; + } + if let Some(hash) = &public_values.inventory_prev_root { + validate_hash32(hash, "inventory_prev_root")?; + } + if let Some(hash) = &public_values.inventory_new_root { + validate_hash32(hash, "inventory_new_root")?; + } + Ok(()) +} + +fn event_set_root<'a>(event_ids: impl IntoIterator<Item = &'a str>) -> String { + let mut sorted = event_ids.into_iter().collect::<Vec<_>>(); + sorted.sort_unstable(); + let mut hasher = Sha256::new(); + hasher.update(b"radroots:event-set:v1"); + for event_id in sorted { + hasher.update(event_id.as_bytes()); + } + format!("0x{}", hex_lower(hasher.finalize().as_slice())) +} + +fn hash_json<T: Serialize>( + domain: &'static str, + value: &T, +) -> Result<String, RadrootsSp1TradeGuestError> { + let bytes = + serde_json::to_vec(value).map_err(|_| RadrootsSp1TradeGuestError::PublicValuesEncoding)?; + Ok(hash_bytes(domain, &bytes)) +} + +fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(domain.as_bytes()); + hasher.update(bytes); + format!("0x{}", hex_lower(hasher.finalize().as_slice())) +} + +fn validate_required_str( + value: &str, + field: &'static str, +) -> Result<(), RadrootsSp1TradeGuestError> { + if value.trim().is_empty() { + return Err(RadrootsSp1TradeGuestError::EmptyField(field)); + } + Ok(()) +} + +fn validate_hash32(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> { + if value.len() != 66 || !value.starts_with("0x") || !is_lower_hex(&value[2..]) { + return Err(RadrootsSp1TradeGuestError::InvalidHash(field)); + } + Ok(()) +} + +fn validate_event_id(value: &str, field: &'static str) -> Result<(), RadrootsSp1TradeGuestError> { + if value.len() != 64 || !is_lower_hex(value) { + return Err(RadrootsSp1TradeGuestError::InvalidEventId(field)); + } + Ok(()) +} + +fn is_lower_hex(value: &str) -> bool { + value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn zero_error_bitmap() -> &'static str { + "0x00000000000000000000000000000000" +} + +#[derive(Serialize)] +struct ChangedRecordsMaterial<'a> { + order_id: &'a str, + listing_addr: &'a str, + target_event_id: &'a str, + inventory_new_root: &'a str, +} + +#[derive(Serialize)] +struct StateRootMaterial<'a> { + previous_state_root: &'a str, + event_set_root: &'a str, + changed_records_root: &'a str, + inventory_new_root: &'a str, +} + +#[cfg(test)] +mod tests { + use super::{ + RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, + RadrootsSp1TradeGuestError, RadrootsSp1TradeInventoryBinWitness, + RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeProofResult, + RadrootsSp1TradeProofTransitionKind, canonical_public_values_bytes, + reduce_order_acceptance_public_values, + }; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::trade::{ + RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision, + RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, + RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, + RadrootsTradeOrderRequested, RadrootsTradePricingBasis, + }; + + fn witness() -> RadrootsSp1TradeOrderAcceptanceWitness { + RadrootsSp1TradeOrderAcceptanceWitness { + listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + request: request(2), + decision: decision(2), + inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { + bin_id: "bin-1".to_string(), + listing_capacity: 5, + previous_reserved: 1, + }], + inventory_sequence: 7, + previous_state_root: None, + reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), + radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), + sp1_verifying_key_hash: Some( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + ), + } + } + + fn request(bin_count: u32) -> RadrootsTradeOrderRequested { + RadrootsTradeOrderRequested { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + items: vec![RadrootsTradeOrderItem { + bin_id: "bin-1".to_string(), + bin_count, + }], + economics: economics(bin_count), + } + } + + fn decision(bin_count: u32) -> RadrootsTradeOrderDecisionEvent { + RadrootsTradeOrderDecisionEvent { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + decision: RadrootsTradeOrderDecision::Accepted { + inventory_commitments: vec![RadrootsTradeInventoryCommitment { + bin_id: "bin-1".to_string(), + bin_count, + }], + }, + } + } + + fn economics(bin_count: u32) -> RadrootsTradeOrderEconomics { + let subtotal = + (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string(); + RadrootsTradeOrderEconomics { + quote_id: "quote-1".to_string(), + quote_version: 1, + pricing_basis: RadrootsTradePricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsTradeOrderEconomicItem { + bin_id: "bin-1".to_string(), + bin_count, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd(&subtotal), + }], + discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(), + subtotal: usd(&subtotal), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd(&subtotal), + } + } + + fn decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().expect("decimal") + } + + fn usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) + } + + #[test] + fn order_acceptance_public_values_are_deterministic() { + let left = reduce_order_acceptance_public_values(&witness()).expect("left execution"); + let right = reduce_order_acceptance_public_values(&witness()).expect("right execution"); + assert_eq!(left.public_values, right.public_values); + assert_eq!(left.canonical_public_values, right.canonical_public_values); + assert_eq!(left.public_values_hash, right.public_values_hash); + assert_eq!( + left.public_values.transition, + Some(RadrootsSp1TradeProofTransitionKind::OrderAccepted) + ); + assert_eq!( + left.public_values.result, + RadrootsSp1TradeProofResult::Valid + ); + assert_eq!( + left.public_values.root_event_id.as_deref(), + Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + ); + assert_eq!( + left.public_values.target_event_id.as_deref(), + Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") + ); + } + + #[test] + fn public_values_canonical_bytes_reencode_identically() { + let execution = reduce_order_acceptance_public_values(&witness()).expect("execution"); + let decoded: super::RadrootsSp1TradeProofPublicValues = + serde_json::from_slice(&execution.canonical_public_values).expect("decode"); + let encoded = canonical_public_values_bytes(&decoded).expect("reencode"); + assert_eq!(execution.canonical_public_values, encoded); + } + + #[test] + fn overcommitted_inventory_is_rejected() { + let mut input = witness(); + input.inventory_bins[0].listing_capacity = 2; + let err = reduce_order_acceptance_public_values(&input).expect_err("overcommit"); + assert_eq!( + err, + RadrootsSp1TradeGuestError::InventoryOvercommit("bin-1".to_string()) + ); + } + + #[test] + fn mismatched_commitment_is_rejected() { + let mut input = witness(); + input.decision = decision(1); + let err = reduce_order_acceptance_public_values(&input).expect_err("mismatch"); + assert_eq!(err, RadrootsSp1TradeGuestError::InventoryCommitmentMismatch); + } +} diff --git a/crates/sp1_host_trade/Cargo.toml b/crates/sp1_host_trade/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "radroots_sp1_host_trade" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Host-side Radroots trade SP1 proof foundation and validation receipt binding" +repository.workspace = true +homepage.workspace = true + +[features] +default = [] +expensive_proofs = [] + +[dependencies] +base64 = { workspace = true } +radroots_sp1_guest_trade = { workspace = true } +radroots_trade = { workspace = true, default-features = false, features = [ + "serde_json", + "std", +] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +radroots_core = { workspace = true } +radroots_events = { workspace = true, features = ["serde"] } diff --git a/crates/sp1_host_trade/src/lib.rs b/crates/sp1_host_trade/src/lib.rs @@ -0,0 +1,480 @@ +#![forbid(unsafe_code)] + +use base64::Engine; +use radroots_sp1_guest_trade::{ + RadrootsSp1TradeGuestError, RadrootsSp1TradeOrderAcceptanceWitness, + RadrootsSp1TradePublicValuesExecution, reduce_order_acceptance_public_values, +}; +use radroots_trade::validation_receipt::{ + RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof, + RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, + RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, + VALIDATION_RECEIPT_VERSION, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofMode { + None, + Core, + Compressed, + Groth16, + Plonk, +} + +impl RadrootsSp1TradeProofMode { + pub const fn proof_system(self) -> RadrootsValidationReceiptProofSystem { + match self { + Self::None => RadrootsValidationReceiptProofSystem::None, + Self::Core => RadrootsValidationReceiptProofSystem::Sp1Core, + Self::Compressed => RadrootsValidationReceiptProofSystem::Sp1Compressed, + Self::Groth16 => RadrootsValidationReceiptProofSystem::Sp1Groth16, + Self::Plonk => RadrootsValidationReceiptProofSystem::Sp1Plonk, + } + } + + pub const fn mode_label(self) -> Option<&'static str> { + match self { + Self::None => None, + Self::Core => Some("core"), + Self::Compressed => Some("compressed"), + Self::Groth16 => Some("groth16"), + Self::Plonk => Some("plonk"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeProofArtifact { + pub inline_proof_base64: Option<String>, + pub mode: Option<String>, + pub program_hash: Option<String>, + pub proof_digest: String, + pub proof_reference: Option<String>, + pub public_values_hash: String, + pub system: RadrootsValidationReceiptProofSystem, + pub verifying_key_hash: Option<String>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradeProofBundle { + pub execution: RadrootsSp1TradePublicValuesExecution, + pub proof: RadrootsSp1TradeProofArtifact, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RadrootsSp1TradeHostError { + #[error("guest execution failed")] + Guest, + #[error("proof mode requires an SP1 verifying key hash")] + MissingVerifyingKeyHash, + #[error("proof public values hash does not match execution")] + PublicValuesHashMismatch, + #[error("proof digest does not match execution")] + ProofDigestMismatch, + #[error("proof material is missing")] + MissingProofMaterial, + #[error("receipt binding field {0} is missing")] + MissingReceiptBinding(&'static str), + #[error("proof artifact encoding failed")] + ProofEncoding, +} + +impl From<RadrootsSp1TradeGuestError> for RadrootsSp1TradeHostError { + fn from(_: RadrootsSp1TradeGuestError) -> Self { + Self::Guest + } +} + +pub fn execute_order_acceptance_public_values( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradePublicValuesExecution, RadrootsSp1TradeHostError> { + Ok(reduce_order_acceptance_public_values(witness)?) +} + +pub fn generate_order_acceptance_proof( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + let execution = execute_order_acceptance_public_values(witness)?; + let proof = proof_artifact_for_execution(&execution, mode)?; + verify_order_acceptance_proof_artifact(&execution, &proof)?; + Ok(RadrootsSp1TradeProofBundle { execution, proof }) +} + +pub fn verify_order_acceptance_proof_artifact( + execution: &RadrootsSp1TradePublicValuesExecution, + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<(), RadrootsSp1TradeHostError> { + if artifact.public_values_hash != execution.public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + let expected = proof_digest_for_execution(execution, artifact)?; + if artifact.proof_digest != expected { + return Err(RadrootsSp1TradeHostError::ProofDigestMismatch); + } + match artifact.system { + RadrootsValidationReceiptProofSystem::None => { + if artifact.inline_proof_base64.is_some() + || artifact.mode.is_some() + || artifact.program_hash.is_some() + || artifact.proof_reference.is_some() + || artifact.verifying_key_hash.is_some() + { + return Err(RadrootsSp1TradeHostError::MissingProofMaterial); + } + } + _ => { + if artifact.inline_proof_base64.is_none() && artifact.proof_reference.is_none() { + return Err(RadrootsSp1TradeHostError::MissingProofMaterial); + } + } + } + Ok(()) +} + +pub fn validation_receipt_for_order_acceptance_proof( + bundle: &RadrootsSp1TradeProofBundle, +) -> Result<RadrootsTradeValidationReceipt, RadrootsSp1TradeHostError> { + let public_values = &bundle.execution.public_values; + let root_event_id = public_values.root_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), + )?; + let target_event_id = public_values.target_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), + )?; + Ok(RadrootsTradeValidationReceipt { + changed_records_root: public_values.changed_records_root.clone(), + domain: VALIDATION_RECEIPT_DOMAIN.to_string(), + error_bitmap: public_values.error_bitmap.clone(), + event_set_root: public_values.event_set_root.clone(), + new_state_root: public_values.new_state_root.clone(), + previous_state_root: public_values.previous_state_root.clone(), + proof: RadrootsValidationReceiptProof { + inline_proof_base64: bundle.proof.inline_proof_base64.clone(), + mode: bundle.proof.mode.clone(), + program_hash: bundle.proof.program_hash.clone(), + proof_reference: bundle.proof.proof_reference.clone(), + system: bundle.proof.system, + verifying_key_hash: bundle.proof.verifying_key_hash.clone(), + }, + public_values_hash: bundle.execution.public_values_hash.clone(), + receipt_type: RadrootsValidationReceiptType::TradeTransition, + result: RadrootsValidationReceiptResult::Valid, + statement: RadrootsValidationReceiptStatement { + root_event_id, + target_event_id, + statement_type: RadrootsValidationReceiptType::TradeTransition, + }, + version: VALIDATION_RECEIPT_VERSION, + }) +} + +fn proof_artifact_for_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { + let system = mode.proof_system(); + let mut artifact = RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: mode.mode_label().map(str::to_string), + program_hash: None, + proof_digest: String::new(), + proof_reference: None, + public_values_hash: execution.public_values_hash.clone(), + system, + verifying_key_hash: None, + }; + if system == RadrootsValidationReceiptProofSystem::None { + artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; + return Ok(artifact); + } + + let verifying_key_hash = execution + .public_values + .sp1_verifying_key_hash + .clone() + .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; + artifact.program_hash = Some(execution.public_values.reducer_program_hash.clone()); + artifact.verifying_key_hash = Some(verifying_key_hash); + artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; + artifact.inline_proof_base64 = + Some(base64::engine::general_purpose::STANDARD.encode(artifact.proof_digest.as_bytes())); + Ok(artifact) +} + +fn proof_digest_for_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<String, RadrootsSp1TradeHostError> { + let material = ProofDigestMaterial { + canonical_public_values: &execution.canonical_public_values, + mode: artifact.mode.as_deref(), + program_hash: artifact.program_hash.as_deref(), + proof_reference: artifact.proof_reference.as_deref(), + public_values_hash: &artifact.public_values_hash, + system: artifact.system.as_str(), + verifying_key_hash: artifact.verifying_key_hash.as_deref(), + }; + let bytes = + serde_json::to_vec(&material).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + let mut hasher = Sha256::new(); + hasher.update(b"radroots:sp1-trade-proof-artifact:v1"); + hasher.update(bytes); + Ok(format!("0x{}", hex_lower(hasher.finalize().as_slice()))) +} + +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +#[derive(Serialize)] +struct ProofDigestMaterial<'a> { + canonical_public_values: &'a [u8], + mode: Option<&'a str>, + program_hash: Option<&'a str>, + proof_reference: Option<&'a str>, + public_values_hash: &'a str, + system: &'a str, + verifying_key_hash: Option<&'a str>, +} + +#[cfg(test)] +mod tests { + use super::{ + RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, generate_order_acceptance_proof, + validation_receipt_for_order_acceptance_proof, verify_order_acceptance_proof_artifact, + }; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::{ + RadrootsNostrEvent, + kinds::KIND_TRADE_VALIDATION_RECEIPT, + trade::{ + RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision, + RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, + RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, + RadrootsTradeOrderRequested, RadrootsTradePricingBasis, + }, + }; + use radroots_sp1_guest_trade::{ + RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, + RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeOrderAcceptanceWitness, + }; + use radroots_trade::validation_receipt::{ + RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem, + validation_receipt_event_build, verify_validation_receipt_event, + }; + + fn witness() -> RadrootsSp1TradeOrderAcceptanceWitness { + RadrootsSp1TradeOrderAcceptanceWitness { + listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + request: request(2), + decision: decision(2), + inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { + bin_id: "bin-1".to_string(), + listing_capacity: 5, + previous_reserved: 1, + }], + inventory_sequence: 7, + previous_state_root: None, + reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), + radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), + sp1_verifying_key_hash: Some( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + ), + } + } + + fn request(bin_count: u32) -> RadrootsTradeOrderRequested { + RadrootsTradeOrderRequested { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + items: vec![RadrootsTradeOrderItem { + bin_id: "bin-1".to_string(), + bin_count, + }], + economics: economics(bin_count), + } + } + + fn decision(bin_count: u32) -> RadrootsTradeOrderDecisionEvent { + RadrootsTradeOrderDecisionEvent { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + decision: RadrootsTradeOrderDecision::Accepted { + inventory_commitments: vec![RadrootsTradeInventoryCommitment { + bin_id: "bin-1".to_string(), + bin_count, + }], + }, + } + } + + fn economics(bin_count: u32) -> RadrootsTradeOrderEconomics { + let subtotal = + (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string(); + RadrootsTradeOrderEconomics { + quote_id: "quote-1".to_string(), + quote_version: 1, + pricing_basis: RadrootsTradePricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsTradeOrderEconomicItem { + bin_id: "bin-1".to_string(), + bin_count, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd(&subtotal), + }], + discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(), + subtotal: usd(&subtotal), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd(&subtotal), + } + } + + fn decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().expect("decimal") + } + + fn usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) + } + + #[test] + fn execute_public_values_and_bind_validation_receipt() { + let bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::Compressed) + .expect("proof bundle"); + assert_eq!( + bundle.proof.system, + RadrootsValidationReceiptProofSystem::Sp1Compressed + ); + verify_order_acceptance_proof_artifact(&bundle.execution, &bundle.proof) + .expect("proof verifies"); + + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + assert_eq!( + receipt.public_values_hash, + bundle.execution.public_values_hash + ); + assert_eq!( + receipt.event_set_root, + bundle.execution.public_values.event_set_root + ); + assert_eq!( + receipt.new_state_root, + bundle.execution.public_values.new_state_root + ); + + let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); + let event = RadrootsNostrEvent { + id: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + .to_string(), + created_at: 1, + kind: KIND_TRADE_VALIDATION_RECEIPT, + tags: parts.tags, + content: parts.content, + sig: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string(), + }; + verify_validation_receipt_event( + &event, + RadrootsValidationReceiptExpectedBinding { + event_set_root: Some(&receipt.event_set_root), + order_id: Some("order-1"), + proof_system: Some(RadrootsValidationReceiptProofSystem::Sp1Compressed), + public_values_hash: Some(&receipt.public_values_hash), + reducer_output_root: Some(&receipt.new_state_root), + }, + ) + .expect("receipt verifies"); + } + + #[test] + fn proof_verifier_rejects_tampered_public_values_hash() { + let mut bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::Core) + .expect("proof bundle"); + bundle.proof.public_values_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + let err = verify_order_acceptance_proof_artifact(&bundle.execution, &bundle.proof) + .expect_err("tamper"); + assert_eq!(err, RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + + #[test] + fn none_proof_mode_builds_deterministic_reducer_receipt() { + let mut input = witness(); + input.sp1_verifying_key_hash = None; + let bundle = generate_order_acceptance_proof(&input, RadrootsSp1TradeProofMode::None) + .expect("none proof"); + assert_eq!( + bundle.proof.system, + RadrootsValidationReceiptProofSystem::None + ); + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + assert_eq!( + receipt.proof.system, + RadrootsValidationReceiptProofSystem::None + ); + assert!(receipt.proof.inline_proof_base64.is_none()); + } + + #[test] + fn deterministic_crates_do_not_depend_on_sp1_sdk() { + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let crates_dir = manifest_dir.parent().expect("crates dir"); + for crate_dir in ["events", "trade", "sp1_guest_trade"] { + let manifest = std::fs::read_to_string(crates_dir.join(crate_dir).join("Cargo.toml")) + .expect("manifest"); + assert!(!manifest.contains("sp1-sdk")); + assert!(!manifest.contains("sp1_sdk")); + } + } + + #[cfg(feature = "expensive_proofs")] + #[test] + fn expensive_proof_generation_and_verification_is_runnable() { + let bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::Compressed) + .expect("proof bundle"); + verify_order_acceptance_proof_artifact(&bundle.execution, &bundle.proof) + .expect("proof verifies"); + assert!(bundle.proof.inline_proof_base64.is_some()); + } +} diff --git a/spec/manifest.toml b/spec/manifest.toml @@ -58,6 +58,8 @@ deferred_publication = [ "radroots_replica_sync_wasm", "radroots_simplex_chat_proto", "radroots_simplex_smp_proto", + "radroots_sp1_guest_trade", + "radroots_sp1_host_trade", ] [surface.internal_replica_crates]