commit 8d1846eb57633e0923cef6902851e27c2bd6e703
parent d819a9f25c67ecae4d3cd437c26c96a8103cbf64
Author: triesap <tyson@radroots.org>
Date: Wed, 1 Jul 2026 10:49:18 +0000
sdk: enforce relay evidence filter boundaries
- carry out-of-filter relay evidence through trade and validation receipts
- require validation receipt inspect results to match the requested event id
- ignore out-of-filter events during receipt and worker evidence selection
- cover resync, list, inspect, and worker evidence rejection paths
Diffstat:
2 files changed, 162 insertions(+), 1 deletion(-)
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -1454,6 +1454,7 @@ pub struct TradeResyncEvidenceReceipt {
pub inserted_count: usize,
pub duplicate_count: usize,
pub malformed_count: usize,
+ pub out_of_filter_count: usize,
pub unsupported_count: usize,
pub eose_count: usize,
pub closed_count: usize,
@@ -1471,6 +1472,7 @@ pub struct TradeResyncEventImportReceipt {
pub duplicate: bool,
pub unsupported: bool,
pub malformed: bool,
+ pub out_of_filter: bool,
pub projection_eligible: bool,
pub verification_status: Option<String>,
pub message: Option<String>,
@@ -1728,6 +1730,7 @@ pub struct TradeValidationReceiptRelayEvidenceReceipt {
pub inserted_count: usize,
pub duplicate_count: usize,
pub malformed_count: usize,
+ pub out_of_filter_count: usize,
pub unsupported_count: usize,
pub eose_count: usize,
pub closed_count: usize,
@@ -3056,6 +3059,7 @@ impl TradeResyncEvidenceReceipt {
inserted_count: receipt.inserted_count,
duplicate_count: receipt.duplicate_count,
malformed_count: receipt.malformed_count,
+ out_of_filter_count: receipt.out_of_filter_count,
unsupported_count: receipt.unsupported_count,
eose_count: receipt.eose_count,
closed_count: receipt.closed_count,
@@ -3076,6 +3080,7 @@ impl From<RadrootsRelayFetchEventReceipt> for TradeResyncEventImportReceipt {
duplicate: receipt.duplicate,
unsupported: receipt.unsupported,
malformed: receipt.malformed,
+ out_of_filter: receipt.out_of_filter,
projection_eligible: receipt.projection_eligible,
verification_status: receipt.verification_status,
message: receipt.message,
@@ -3297,6 +3302,8 @@ where
)
.await?;
let (mut receipts, mut invalid_receipts) = classify_validation_receipts(events, None)?;
+ receipts.retain(|receipt| receipt.event.id == receipt_event_id.as_str());
+ invalid_receipts.retain(|receipt| receipt.event.id == receipt_event_id.as_str());
attach_worker_evidence(
sdk,
adapter,
@@ -3413,6 +3420,7 @@ async fn validation_receipt_events_from_fetch(
let mut event_ids = events
.iter()
.filter(|event| !event.malformed)
+ .filter(|event| !event.out_of_filter)
.filter_map(|event| event.event_id.as_deref())
.collect::<BTreeSet<_>>()
.into_iter()
@@ -3662,6 +3670,7 @@ impl TradeValidationReceiptRelayEvidenceReceipt {
inserted_count: receipt.inserted_count,
duplicate_count: receipt.duplicate_count,
malformed_count: receipt.malformed_count,
+ out_of_filter_count: receipt.out_of_filter_count,
unsupported_count: receipt.unsupported_count,
eose_count: receipt.eose_count,
closed_count: receipt.closed_count,
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -1196,7 +1196,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
#[cfg(feature = "relay-runtime")]
#[tokio::test]
async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
+ let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
let order_id = "trade-validation-receipts-sdk";
let listing_event_id = deterministic_event_id("validation-receipt-listing");
let root_event_id = deterministic_event_id("validation-receipt-request");
@@ -1219,10 +1219,21 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
34,
);
let worker_event_id = worker_raw_event.id.to_hex();
+ let wrong_root_event_id = deterministic_event_id("validation-receipt-worker-wrong-root");
+ let wrong_worker_raw_event = signed_raw_worker_result_event(
+ order_id,
+ &receipt_event_id,
+ &listing_event_id,
+ &wrong_root_event_id,
+ &target_event_id,
+ 35,
+ );
+ let wrong_worker_event_id = wrong_worker_raw_event.id.to_hex();
let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX);
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
relay_raw_event_item(&receipt_raw_event, RELAY, 4_000),
relay_raw_event_item(&worker_raw_event, RELAY, 4_001),
+ relay_raw_event_item(&wrong_worker_raw_event, RELAY, 4_002),
relay_eose(RELAY),
]);
@@ -1242,6 +1253,7 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
assert_eq!(list.relay_targets, vec![RELAY.to_owned()]);
assert_eq!(list.receipts.len(), 1);
assert!(list.invalid_receipts.is_empty());
+ assert_eq!(list.relay_evidence.out_of_filter_count, 2);
assert_eq!(
list.receipts[0].event.id.as_str(),
receipt_event_id.as_str()
@@ -1264,6 +1276,13 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
.map(|confidence| confidence.as_str()),
Some("committed_by_trusted_service")
);
+ assert!(
+ store
+ .get_event(wrong_worker_event_id.as_str())
+ .await
+ .expect("wrong worker event lookup")
+ .is_none()
+ );
let inspect = sdk
.trades()
@@ -1291,6 +1310,102 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
#[cfg(feature = "relay-runtime")]
#[tokio::test]
+async fn trade_validation_receipt_list_rejects_out_of_filter_order_receipts() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
+ let requested_order_id = "validation-receipt-list-requested";
+ let unrelated_order_id = "validation-receipt-list-unrelated";
+ let unrelated_receipt = signed_raw_validation_receipt_event(
+ unrelated_order_id,
+ &deterministic_event_id("validation-receipt-list-unrelated-listing"),
+ &deterministic_event_id("validation-receipt-list-unrelated-request"),
+ &deterministic_event_id("validation-receipt-list-unrelated-decision"),
+ 36,
+ );
+ let unrelated_receipt_id =
+ RadrootsEventId::parse(unrelated_receipt.id.to_hex()).expect("unrelated receipt id");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ relay_raw_event_item(&unrelated_receipt, RELAY, 4_010),
+ relay_eose(RELAY),
+ ]);
+
+ let list = sdk
+ .trades()
+ .validation_receipts()
+ .list_with_fetch_adapter(
+ TradeValidationReceiptListRequest::parse(requested_order_id).expect("list request"),
+ &adapter,
+ )
+ .await
+ .expect("validation receipt list");
+
+ assert!(list.receipts.is_empty());
+ assert!(list.invalid_receipts.is_empty());
+ assert_eq!(list.relay_evidence.inserted_count, 0);
+ assert_eq!(list.relay_evidence.out_of_filter_count, 1);
+ assert!(list.relay_evidence.events[0].out_of_filter);
+ assert!(
+ store
+ .get_event(unrelated_receipt_id.as_str())
+ .await
+ .expect("unrelated receipt lookup")
+ .is_none()
+ );
+}
+
+#[cfg(feature = "relay-runtime")]
+#[tokio::test]
+async fn trade_validation_receipt_inspect_rejects_unrequested_relay_receipts() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
+ let requested_receipt = signed_raw_validation_receipt_event(
+ "validation-receipt-inspect-requested",
+ &deterministic_event_id("validation-receipt-inspect-requested-listing"),
+ &deterministic_event_id("validation-receipt-inspect-requested-request"),
+ &deterministic_event_id("validation-receipt-inspect-requested-decision"),
+ 37,
+ );
+ let requested_receipt_id =
+ RadrootsEventId::parse(requested_receipt.id.to_hex()).expect("requested receipt id");
+ let unrelated_receipt = signed_raw_validation_receipt_event(
+ "validation-receipt-inspect-unrelated",
+ &deterministic_event_id("validation-receipt-inspect-unrelated-listing"),
+ &deterministic_event_id("validation-receipt-inspect-unrelated-request"),
+ &deterministic_event_id("validation-receipt-inspect-unrelated-decision"),
+ 38,
+ );
+ let unrelated_receipt_id =
+ RadrootsEventId::parse(unrelated_receipt.id.to_hex()).expect("unrelated receipt id");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ relay_raw_event_item(&unrelated_receipt, RELAY, 4_020),
+ relay_eose(RELAY),
+ ]);
+
+ let inspect = sdk
+ .trades()
+ .validation_receipts()
+ .inspect_with_fetch_adapter(
+ TradeValidationReceiptInspectRequest::new(requested_receipt_id.clone()),
+ &adapter,
+ )
+ .await
+ .expect("validation receipt inspect");
+
+ assert_eq!(inspect.receipt_event_id, requested_receipt_id);
+ assert!(inspect.receipt.is_none());
+ assert!(inspect.invalid_receipt.is_none());
+ assert_eq!(inspect.relay_evidence.inserted_count, 0);
+ assert_eq!(inspect.relay_evidence.out_of_filter_count, 1);
+ assert!(inspect.relay_evidence.events[0].out_of_filter);
+ assert!(
+ store
+ .get_event(unrelated_receipt_id.as_str())
+ .await
+ .expect("unrelated receipt lookup")
+ .is_none()
+ );
+}
+
+#[cfg(feature = "relay-runtime")]
+#[tokio::test]
async fn trade_resync_imports_relay_evidence_into_empty_local_store() {
let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
let request_event = signed_raw_order_request_event("resync-empty-local-import", 41);
@@ -1393,6 +1508,43 @@ async fn trade_resync_reports_malformed_evidence_without_poisoning_store() {
#[cfg(feature = "relay-runtime")]
#[tokio::test]
+async fn trade_resync_rejects_out_of_filter_evidence_without_poisoning_store() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
+ let unrelated_event = signed_raw_order_request_event("resync-out-of-filter-unrelated", 43);
+ let unrelated_event_id =
+ RadrootsEventId::parse(unrelated_event.id.to_hex()).expect("unrelated event id");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ relay_raw_event_item(&unrelated_event, RELAY, 5_150),
+ relay_eose(RELAY),
+ ]);
+
+ let resync = sdk
+ .trades()
+ .resync()
+ .resync_with_fetch_adapter(
+ TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
+ "resync-out-of-filter-requested",
+ ))),
+ &adapter,
+ )
+ .await
+ .expect("resync");
+
+ assert_eq!(resync.status.status, TradeStatusKind::Missing);
+ assert_eq!(resync.evidence.inserted_count, 0);
+ assert_eq!(resync.evidence.out_of_filter_count, 1);
+ assert!(resync.evidence.events[0].out_of_filter);
+ assert!(
+ store
+ .get_event(unrelated_event_id.as_str())
+ .await
+ .expect("unrelated event lookup")
+ .is_none()
+ );
+}
+
+#[cfg(feature = "relay-runtime")]
+#[tokio::test]
async fn trade_resync_errors_on_total_relay_failure() {
let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY, RELAY_B]).await;
let adapter = RadrootsMockRelayFetchAdapter::new(vec![