commit ff3b3a20957793146bb10181c824166dcc20fdbe
parent 97efeccbd63deb0ff085524ddb69cad9b9261240
Author: triesap <tyson@radroots.org>
Date: Sat, 28 Mar 2026 22:51:38 +0000
trade_listing: dedupe replayed validation requests
Diffstat:
2 files changed, 151 insertions(+), 20 deletions(-)
diff --git a/src/features/trade_listing/handlers/dvm.rs b/src/features/trade_listing/handlers/dvm.rs
@@ -452,6 +452,13 @@ async fn handle_listing_validate_request(
client: &RadrootsNostrClient,
state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<(), TradeListingDvmError> {
+ {
+ let state = state.lock().await;
+ if state.is_non_order_event_seen(&event.id.to_string()) {
+ return Ok(());
+ }
+ }
+
let listing_event = if let Some(ptr) = payload.listing_event {
match fetch_event_by_id_io(client, &ptr.id).await {
Ok(evt) => Some(evt),
@@ -525,7 +532,12 @@ async fn handle_listing_validate_request(
}
}
- send_validate_result(event, client, listing_addr, errors).await
+ send_validate_result(event, client, listing_addr, errors).await?;
+ state
+ .lock()
+ .await
+ .mark_non_order_event_seen(&event.id.to_string());
+ Ok(())
}
async fn send_validate_result(
@@ -1898,10 +1910,10 @@ mod tests {
let client = make_client(&rhi_keys);
let listing_addr = listing_addr_for_seller(&seller_keys);
let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let event = make_event(
+ let missing_event = make_event(
&seller_keys,
RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
- "content".to_string(),
+ "missing".to_string(),
Vec::new(),
);
@@ -1914,27 +1926,51 @@ mod tests {
}),
};
assert!(
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
- .await
- .is_ok()
+ handle_listing_validate_request(
+ &missing_event,
+ payload,
+ &listing_addr,
+ &client,
+ &state
+ )
+ .await
+ .is_ok()
);
+ let fetch_error_event = make_event(
+ &seller_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ "fetch-error".to_string(),
+ Vec::new(),
+ );
push_fetch_events_ok(Vec::new());
push_send_ok();
let payload = TradeListingValidateRequest {
listing_event: None,
};
assert!(
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
- .await
- .is_ok()
+ handle_listing_validate_request(
+ &fetch_error_event,
+ payload,
+ &listing_addr,
+ &client,
+ &state,
+ )
+ .await
+ .is_ok()
);
+ let success_event = make_event(
+ &seller_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ "success".to_string(),
+ Vec::new(),
+ );
dvm_test_hooks()
.lock()
.expect("hooks")
.fetch_event_by_id_results
- .push_back(Ok(event.clone()));
+ .push_back(Ok(success_event.clone()));
push_validate_listing_ok(
listing_addr.clone(),
RadrootsListingFarmRef {
@@ -1946,27 +1982,39 @@ mod tests {
push_send_ok();
let payload = TradeListingValidateRequest {
listing_event: Some(RadrootsNostrEventPtr {
- id: event.id.to_hex(),
+ id: success_event.id.to_hex(),
relays: None,
}),
};
assert!(
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
- .await
- .is_ok()
+ handle_listing_validate_request(
+ &success_event,
+ payload,
+ &listing_addr,
+ &client,
+ &state
+ )
+ .await
+ .is_ok()
);
assert!(state.lock().await.is_listing_validated(&listing_addr));
assert_eq!(
state.lock().await.validated_listing_event_id(&listing_addr),
- Some(event.id.to_string().as_str())
+ Some(success_event.id.to_string().as_str())
);
let other_listing_addr = listing_addr_for_seller(&rhi_keys);
+ let mismatch_event = make_event(
+ &seller_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ "mismatch".to_string(),
+ Vec::new(),
+ );
dvm_test_hooks()
.lock()
.expect("hooks")
.fetch_event_by_id_results
- .push_back(Ok(event.clone()));
+ .push_back(Ok(mismatch_event.clone()));
push_validate_listing_ok(
other_listing_addr,
RadrootsListingFarmRef {
@@ -1977,14 +2025,14 @@ mod tests {
push_send_ok();
let payload = TradeListingValidateRequest {
listing_event: Some(RadrootsNostrEventPtr {
- id: event.id.to_hex(),
+ id: mismatch_event.id.to_hex(),
relays: None,
}),
};
let mismatch_listing_addr = listing_addr_for_seller(&buyer_keys);
assert!(
handle_listing_validate_request(
- &event,
+ &mismatch_event,
payload,
&mismatch_listing_addr,
&client,
@@ -2004,13 +2052,19 @@ mod tests {
.lock()
.await
.mark_listing_validated(&listing_addr, "stale-listing-event");
+ let stale_event = make_event(
+ &seller_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ "stale".to_string(),
+ Vec::new(),
+ );
push_fetch_events_ok(Vec::new());
push_send_ok();
let payload = TradeListingValidateRequest {
listing_event: None,
};
assert!(
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
+ handle_listing_validate_request(&stale_event, payload, &listing_addr, &client, &state)
.await
.is_ok()
);
@@ -2018,6 +2072,65 @@ mod tests {
}
#[tokio::test]
+ async fn handle_listing_validate_request_dedupes_replayed_request_event() {
+ let _guard = test_guard();
+ let (rhi_keys, seller_keys, _) = make_keys();
+ let client = make_client(&rhi_keys);
+ let listing_addr = listing_addr_for_seller(&seller_keys);
+ let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
+ let event = make_event(
+ &seller_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ "content".to_string(),
+ Vec::new(),
+ );
+ let payload = TradeListingValidateRequest {
+ listing_event: Some(RadrootsNostrEventPtr {
+ id: event.id.to_hex(),
+ relays: None,
+ }),
+ };
+
+ dvm_test_hooks()
+ .lock()
+ .expect("hooks")
+ .fetch_event_by_id_results
+ .push_back(Ok(event.clone()));
+ push_validate_listing_ok(
+ listing_addr.clone(),
+ RadrootsListingFarmRef {
+ pubkey: seller_keys.public_key().to_hex(),
+ d_tag: "farmtag".to_string(),
+ },
+ );
+ push_farm_validation_result(Ok(Vec::new()));
+ push_send_ok();
+ assert!(
+ handle_listing_validate_request(
+ &event,
+ payload.clone(),
+ &listing_addr,
+ &client,
+ &state,
+ )
+ .await
+ .is_ok()
+ );
+ assert!(
+ state
+ .lock()
+ .await
+ .is_non_order_event_seen(&event.id.to_string())
+ );
+
+ assert!(
+ handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
+ .await
+ .is_ok()
+ );
+ }
+
+ #[tokio::test]
async fn handler_paths_cover_state_transitions() {
let _guard = test_guard();
let (rhi_keys, seller_keys, buyer_keys) = make_keys();
diff --git a/src/features/trade_listing/state.rs b/src/features/trade_listing/state.rs
@@ -35,6 +35,8 @@ pub struct TradeListingState {
validated_listings: HashSet<String>,
#[serde(default)]
validated_listing_events: HashMap<String, ValidatedListingState>,
+ #[serde(default)]
+ seen_non_order_event_ids: HashSet<String>,
orders: HashMap<String, TradeOrderState>,
last_event_created_at: Option<u32>,
}
@@ -190,6 +192,14 @@ impl TradeListingState {
.unwrap_or(false)
}
+ pub fn mark_non_order_event_seen(&mut self, event_id: &str) -> bool {
+ self.seen_non_order_event_ids.insert(event_id.to_string())
+ }
+
+ pub fn is_non_order_event_seen(&self, event_id: &str) -> bool {
+ self.seen_non_order_event_ids.contains(event_id)
+ }
+
pub fn observe_event_created_at(&mut self, created_at: u32) {
self.last_event_created_at = Some(
self.last_event_created_at
@@ -303,7 +313,7 @@ mod tests {
ValidatedListingState,
};
use radroots_trade::listing::order::TradeOrderStatus;
- use std::collections::HashMap;
+ use std::collections::{HashMap, HashSet};
fn unique_state_path(suffix: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
@@ -336,6 +346,9 @@ mod tests {
assert!(!state.is_event_seen("order-1", "evt"));
assert!(state.mark_event_seen("order-1", "evt"));
assert!(state.is_event_seen("order-1", "evt"));
+ assert!(!state.is_non_order_event_seen("evt-non-order"));
+ assert!(state.mark_non_order_event_seen("evt-non-order"));
+ assert!(state.is_non_order_event_seen("evt-non-order"));
assert_eq!(state.replay_since(1_000, 300, 60), 700);
state.observe_event_created_at(900);
@@ -350,6 +363,7 @@ mod tests {
assert!(state.get_order_mut("missing").is_none());
assert!(!state.mark_event_seen("missing", "evt-1"));
assert!(!state.is_event_seen("missing", "evt-1"));
+ assert!(!state.is_non_order_event_seen("evt-2"));
assert_eq!(
TradeListingStateError::MissingOrder.to_string(),
@@ -394,6 +408,7 @@ mod tests {
let state_handle = runtime.state();
let mut state = state_handle.lock().await;
state.mark_listing_validated("addr", "evt-listing-1");
+ state.mark_non_order_event_seen("evt-validate-1");
state.observe_event_created_at(456);
}
runtime.persist().await.expect("persist");
@@ -406,6 +421,7 @@ mod tests {
loaded_state.validated_listing_event_id("addr"),
Some("evt-listing-1")
);
+ assert!(loaded_state.is_non_order_event_seen("evt-validate-1"));
assert_eq!(loaded_state.last_event_created_at(), Some(456));
let _ = tokio::fs::remove_file(path).await;
@@ -445,6 +461,7 @@ mod tests {
state: TradeListingState {
validated_listings: ["addr".to_string()].into_iter().collect(),
validated_listing_events: HashMap::new(),
+ seen_non_order_event_ids: HashSet::new(),
orders: HashMap::new(),
last_event_created_at: Some(321),
},
@@ -479,6 +496,7 @@ mod tests {
event_id: "evt-listing-1".to_string(),
},
)]),
+ seen_non_order_event_ids: HashSet::new(),
orders: HashMap::new(),
last_event_created_at: None,
};