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:
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!(