From d83247813db56d24633b148e7940a5a8f703b00c Mon Sep 17 00:00:00 2001 From: tinfoil hash <238757206+tinfoilhash@users.noreply.github.com> Date: Thu, 12 Nov 2026 09:31:53 -0500 Subject: [PATCH] ignore events relays send that weren't asked for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a relay could answer a lookup with a verification of a different file, or any other signed event, and it would be listed as verifying this one. like the web page, whose relay pool drops events that don't match the subscription's filters, every client now has nostr-sdk check that each event matches what was asked for. > made with claude code + tinfoil hash ยท session `cac618d4-34cf-4f09-bff6-b4a25528953f` Co-Authored-By: Claude Opus 5.5 --- src/file_event_publisher.rs | 2 +- src/profile_fetcher.rs | 14 ++++++++++++-- src/relay_connector.rs | 6 ++++++ src/test_relays.rs | 18 ++++++++++++++++-- src/verification_fetcher.rs | 17 +++++++++++++++-- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/file_event_publisher.rs b/src/file_event_publisher.rs index d017066..0214a83 100644 --- a/src/file_event_publisher.rs +++ b/src/file_event_publisher.rs @@ -24,7 +24,7 @@ pub struct FileEventPublisher { impl FileEventPublisher { pub async fn connect(relay_config: &RelayConfig) -> Result { - let client = Client::default(); + let client = relay_connector::build_client(); let relay_urls = relay_connector::connect_all(&client, &relay_config.file_relay_urls).await; if relay_urls.is_empty() { diff --git a/src/profile_fetcher.rs b/src/profile_fetcher.rs index 58098f2..5da21d0 100644 --- a/src/profile_fetcher.rs +++ b/src/profile_fetcher.rs @@ -11,7 +11,7 @@ const PROFILE_TIMEOUT: Duration = Duration::from_secs(3); // the newest profile of the user that the relays return in time. each relay is // asked on its own, so one that's slow to connect doesn't hide another's answer. pub async fn fetch(public_key: PublicKey, relay_urls: &[RelayUrl]) -> Option { - let client = Client::default(); + let client = relay_connector::build_client(); let filter = Filter::new() .kind(Kind::Metadata) .author(public_key) @@ -56,7 +56,7 @@ pub async fn fetch(public_key: PublicKey, relay_urls: &[RelayUrl]) -> Option Client { + Client::builder().verify_subscriptions(true).build() +} + // a relay that fails to connect isn't retried, so callers only use the ones // that connect. pub async fn connect(client: &Client, url: &RelayUrl) -> Option { diff --git a/src/test_relays.rs b/src/test_relays.rs index a396bc3..2e945ba 100644 --- a/src/test_relays.rs +++ b/src/test_relays.rs @@ -2,8 +2,9 @@ use crate::relay_config::RelayConfig; use nostr_sdk::prelude::*; use std::time::Duration; -// relays for tests: one running in the test process, seeded with events, and -// one that can't be reached. +// relays for tests: one running in the test process, seeded with events, one +// that answers with events that weren't asked for, and one that can't be +// reached. // nothing listens on port 1, so connecting fails right away. pub const DEAD_RELAY_URL: &str = "ws://127.0.0.1:1"; @@ -27,6 +28,19 @@ pub async fn run_relay(events: &[Event]) -> (MockRelay, String) { (relay, url.to_string()) } +// a relay that answers every request with events that don't match it. +pub async fn run_mismatching_relay() -> (MockRelay, String) { + let relay = MockRelay::run_with_opts(LocalRelayTestOptions { + unresponsive_connection: None, + send_random_events: true, + }) + .await + .unwrap(); + let url = relay.url().await; + + (relay, url.to_string()) +} + pub fn relay_url(url: &str) -> RelayUrl { RelayUrl::parse(url).unwrap() } diff --git a/src/verification_fetcher.rs b/src/verification_fetcher.rs index 9258db3..1cfed08 100644 --- a/src/verification_fetcher.rs +++ b/src/verification_fetcher.rs @@ -188,7 +188,7 @@ async fn coordinate( sender: mpsc::Sender>, ) { // the client shuts its relays down when dropped, so it's held until the end. - let client = Client::default(); + let client = relay_connector::build_client(); let mut progress = Progress::new(&stages_by_url, user_requirement); let mut has_loaded = false; let mut has_found_user_profile = false; @@ -395,7 +395,9 @@ mod tests { use crate::test_event_builders::{ HASH, build_event, build_file_event, build_follow_list, build_profile, }; - use crate::test_relays::{DEAD_RELAY_URL, build_relay_config, relay_url, run_relay}; + use crate::test_relays::{ + DEAD_RELAY_URL, build_relay_config, relay_url, run_mismatching_relay, run_relay, + }; mod group_stages_by_url { use super::*; @@ -634,6 +636,17 @@ mod tests { assert_eq!(received, [Received::Loaded]); } + #[tokio::test] + async fn ignores_events_a_relay_sends_that_werent_asked_for() { + let (_relay, url) = run_mismatching_relay().await; + + let received = fetch_all(None, build_relay_config(&[&url], &[&url])) + .await + .unwrap(); + + assert_eq!(received, [Received::Loaded]); + } + #[tokio::test] async fn finishes_with_a_repeated_relay_url() { let alice = Keys::generate();