lib

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

commit 9ae7510ecc46c2f703bffa80222701ca689da76b
parent 48885bf9df86dcbcae561014c3da37b566f6191e
Author: triesap <tyson@radroots.org>
Date:   Thu, 21 May 2026 05:27:27 +0000

validation: bind receipts to listing ids

- add listing_event_id to validation receipt statements, tags, and expected bindings
- bind host proof receipts and proof envelopes to listing ids before verification
- extend receipt round trip coverage for the canonical listing marker
- validate with cargo fmt and focused radroots_trade/radroots_sp1_host_trade tests

Diffstat:
Mcrates/sp1_host_trade/src/lib.rs | 28++++++++++++++++++++++++++++
Mcrates/trade/src/validation_receipt.rs | 34+++++++++++++++++++++++++++++++++-
2 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/crates/sp1_host_trade/src/lib.rs b/crates/sp1_host_trade/src/lib.rs @@ -85,6 +85,7 @@ pub struct RadrootsSp1TradeProofEnvelope { pub sp1_verifying_key_hash: String, pub receipt_type: String, pub receipt_result: String, + pub listing_event_id: String, pub root_event_id: String, pub target_event_id: String, pub event_set_root: String, @@ -499,6 +500,13 @@ fn verify_validation_receipt_matches_public_values( "error_bitmap", )); } + if public_values.listing_event_id.as_deref() + != Some(receipt.statement.listing_event_id.as_str()) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "listing_event_id", + )); + } if public_values.root_event_id.as_deref() != Some(receipt.statement.root_event_id.as_str()) { return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( "root_event_id", @@ -613,6 +621,9 @@ pub fn validation_receipt_for_order_acceptance_proof( bundle: &RadrootsSp1TradeProofBundle, ) -> Result<RadrootsTradeValidationReceipt, RadrootsSp1TradeHostError> { let public_values = &bundle.execution.public_values; + let listing_event_id = public_values.listing_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), + )?; let root_event_id = public_values.root_event_id.clone().ok_or( RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), )?; @@ -638,6 +649,7 @@ pub fn validation_receipt_for_order_acceptance_proof( receipt_type: RadrootsValidationReceiptType::TradeTransition, result: validation_receipt_result_from_public_values(public_values.result), statement: RadrootsValidationReceiptStatement { + listing_event_id, root_event_id, target_event_id, statement_type: RadrootsValidationReceiptType::TradeTransition, @@ -786,6 +798,9 @@ fn proof_envelope_for_real_sp1_execution( validation_receipt_result_from_public_values(execution.public_values.result), ) .to_owned(), + listing_event_id: execution.public_values.listing_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), + )?, root_event_id: execution.public_values.root_event_id.clone().ok_or( RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), )?, @@ -839,6 +854,7 @@ fn proof_digest_for_envelope( sp1_verifying_key_hash: envelope.sp1_verifying_key_hash.as_str(), receipt_type: envelope.receipt_type.as_str(), receipt_result: envelope.receipt_result.as_str(), + listing_event_id: envelope.listing_event_id.as_str(), root_event_id: envelope.root_event_id.as_str(), target_event_id: envelope.target_event_id.as_str(), event_set_root: envelope.event_set_root.as_str(), @@ -904,6 +920,17 @@ fn verify_proof_envelope( "result", )); } + if envelope.listing_event_id.as_str() + != execution + .public_values + .listing_event_id + .as_deref() + .unwrap_or("") + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "listing_event_id", + )); + } if envelope.root_event_id.as_str() != execution .public_values @@ -1133,6 +1160,7 @@ struct ProofEnvelopeDigestMaterial<'a> { sp1_verifying_key_hash: &'a str, receipt_type: &'a str, receipt_result: &'a str, + listing_event_id: &'a str, root_event_id: &'a str, target_event_id: &'a str, event_set_root: &'a str, diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -112,6 +112,7 @@ impl RadrootsValidationReceiptProofSystem { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RadrootsValidationReceiptStatement { + pub listing_event_id: String, pub root_event_id: String, pub target_event_id: String, #[serde(rename = "type")] @@ -149,6 +150,7 @@ pub struct RadrootsTradeValidationReceipt { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsValidationReceiptTags { pub event_set_root: String, + pub listing_event_id: String, pub order_id: String, pub proof_system: RadrootsValidationReceiptProofSystem, pub public_values_hash: String, @@ -161,6 +163,7 @@ pub struct RadrootsValidationReceiptTags { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct RadrootsValidationReceiptExpectedBinding<'a> { pub event_set_root: Option<&'a str>, + pub listing_event_id: Option<&'a str>, pub order_id: Option<&'a str>, pub program_hash: Option<&'a str>, pub proof_system: Option<RadrootsValidationReceiptProofSystem>, @@ -222,6 +225,10 @@ impl RadrootsTradeValidationReceipt { validate_hash32(&self.new_state_root, "new_state_root")?; validate_hash32(&self.previous_state_root, "previous_state_root")?; validate_hash32(&self.public_values_hash, "public_values_hash")?; + validate_event_id( + &self.statement.listing_event_id, + "statement.listing_event_id", + )?; validate_event_id(&self.statement.root_event_id, "statement.root_event_id")?; validate_event_id(&self.statement.target_event_id, "statement.target_event_id")?; validate_result_error_bitmap(self.result, &self.error_bitmap)?; @@ -313,6 +320,13 @@ pub fn validation_receipt_tags( vec![TAG_D.to_string(), order_id.to_string()], vec![ "e".to_string(), + receipt.statement.listing_event_id.clone(), + String::new(), + String::new(), + "listing".to_string(), + ], + vec![ + "e".to_string(), receipt.statement.root_event_id.clone(), String::new(), String::new(), @@ -352,6 +366,7 @@ pub fn validation_receipt_tags_from_tags( tags: &[Vec<String>], ) -> Result<RadrootsValidationReceiptTags, RadrootsValidationReceiptError> { let order_id = required_tag_value(tags, TAG_D)?; + let listing_event_id = required_event_marker(tags, "listing")?; let root_event_id = required_event_marker(tags, "root")?; let target_event_id = required_event_marker(tags, "target")?; let event_set_root = required_tag_value(tags, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; @@ -372,6 +387,7 @@ pub fn validation_receipt_tags_from_tags( TAG_VALIDATION_RECEIPT_RECEIPT_TYPE, ))?; + validate_event_id(&listing_event_id, "tags.e.listing")?; validate_event_id(&root_event_id, "tags.e.root")?; validate_event_id(&target_event_id, "tags.e.target")?; validate_hash32(&event_set_root, TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT)?; @@ -386,6 +402,7 @@ pub fn validation_receipt_tags_from_tags( Ok(RadrootsValidationReceiptTags { event_set_root, + listing_event_id, order_id, proof_system, public_values_hash, @@ -430,6 +447,11 @@ pub fn verify_validation_receipt_event( let receipt = validation_receipt_content_from_str(&event.content)?; let tags = validation_receipt_tags_from_tags(&event.tags)?; + if tags.listing_event_id != receipt.statement.listing_event_id { + return Err(RadrootsValidationReceiptError::TagMismatch( + "listing_event_id", + )); + } if tags.root_event_id != receipt.statement.root_event_id { return Err(RadrootsValidationReceiptError::TagMismatch("root_event_id")); } @@ -486,6 +508,13 @@ fn validate_expected_binding( )); } } + if let Some(listing_event_id) = expected.listing_event_id { + if tags.listing_event_id != listing_event_id { + return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( + "listing_event_id", + )); + } + } if let Some(event_set_root) = expected.event_set_root { if tags.event_set_root != event_set_root { return Err(RadrootsValidationReceiptError::ExpectedBindingMismatch( @@ -720,6 +749,7 @@ mod tests { receipt_type: RadrootsValidationReceiptType::TradeTransition, result: RadrootsValidationReceiptResult::Valid, statement: RadrootsValidationReceiptStatement { + listing_event_id: event_id('0'), root_event_id: event_id('1'), target_event_id: event_id('2'), statement_type: RadrootsValidationReceiptType::TradeTransition, @@ -762,12 +792,13 @@ mod tests { assert_eq!( content, format!( - "{{\"changed_records_root\":\"{}\",\"domain\":\"radroots.receipt\",\"error_bitmap\":\"0x00000000000000000000000000000000\",\"event_set_root\":\"{}\",\"new_state_root\":\"{}\",\"previous_state_root\":\"{}\",\"proof\":{{\"inline_proof_base64\":null,\"mode\":null,\"program_hash\":null,\"proof_reference\":null,\"system\":\"none\",\"verifying_key_hash\":null}},\"public_values_hash\":\"{}\",\"receipt_type\":\"trade_transition\",\"result\":\"valid\",\"statement\":{{\"root_event_id\":\"{}\",\"target_event_id\":\"{}\",\"type\":\"trade_transition\"}},\"version\":1}}", + "{{\"changed_records_root\":\"{}\",\"domain\":\"radroots.receipt\",\"error_bitmap\":\"0x00000000000000000000000000000000\",\"event_set_root\":\"{}\",\"new_state_root\":\"{}\",\"previous_state_root\":\"{}\",\"proof\":{{\"inline_proof_base64\":null,\"mode\":null,\"program_hash\":null,\"proof_reference\":null,\"system\":\"none\",\"verifying_key_hash\":null}},\"public_values_hash\":\"{}\",\"receipt_type\":\"trade_transition\",\"result\":\"valid\",\"statement\":{{\"listing_event_id\":\"{}\",\"root_event_id\":\"{}\",\"target_event_id\":\"{}\",\"type\":\"trade_transition\"}},\"version\":1}}", hash32('6'), hash32('c'), hash32('4'), hash32('3'), receipt.public_values_hash, + event_id('0'), event_id('1'), event_id('2'), ) @@ -781,6 +812,7 @@ mod tests { assert_eq!(event.kind, KIND_TRADE_VALIDATION_RECEIPT); let verified = validation_receipt_from_event(&event).expect("verified receipt"); assert_eq!(verified.tags.order_id, "order-1"); + assert_eq!(verified.tags.listing_event_id, event_id('0')); assert_eq!(verified.tags.event_set_root, hash32('c')); assert_eq!(verified.tags.reducer_output_root, hash32('4')); assert_eq!(