Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions nostr-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@

-->

## Unreleased

### Deprecated

- Deprecate `LocalRelayBuilder::max_query_results` and `LocalRelayBuilder::default_filter_limit` (https://github.com/nostrdevkit/nostr/pull/1461)

## v0.45.2 - 2026/08/19

### Fixed
Expand Down
47 changes: 18 additions & 29 deletions nostr-sdk/src/local_relay/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use super::local::LocalRelay;
pub(super) const DEFAULT_MAX_PENDING_HANDSHAKES: usize = 128;
pub(super) const DEFAULT_MAX_FILTERS_PER_REQ: usize = 20;
pub(super) const DEFAULT_MAX_EVENT_SIZE: usize = 64 * 1024;
pub(super) const DEFAULT_MAX_QUERY_RESULTS: usize = 500;
pub(super) const DEFAULT_MAX_NEGENTROPY_ITEMS: usize = 50_000;
pub(super) const DEFAULT_MAX_SUBSCRIPTION_BYTES: usize = 1024 * 1024;
pub(super) const DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE: usize = 5 * 1024 * 1024;
Expand Down Expand Up @@ -296,18 +295,14 @@ pub struct LocalRelayBuilder {
pub(crate) max_subid_length: usize,
/// Max filters in one REQ
pub(crate) max_filters_per_req: usize,
/// Maximum records that can be returned per filter
pub(crate) max_filter_limit: usize,
/// Max total bytes retained by active subscriptions per connection
pub(crate) max_subscription_bytes: usize,
/// Max active negentropy subscriptions per connection
pub(crate) max_negentropy_subscriptions: usize,
/// Max total negentropy items retained per connection
pub(crate) max_negentropy_items: usize,
/// Max filter's limit
pub(crate) max_filter_limit: Option<usize>,
/// Max aggregate query results
pub(crate) max_query_results: usize,
/// Default filter's limit if there is no limit
pub(crate) default_filter_limit: usize,
/// Enables NIP-42 authentication for kind 1059 (GiftWrap), ensuring the
/// authenticated pubkey is the only "p" tag
pub(crate) auth_dm: bool,
Expand Down Expand Up @@ -351,12 +346,10 @@ impl Default for LocalRelayBuilder {
websocket_handshake_timeout: DEFAULT_WEBSOCKET_HANDSHAKE_TIMEOUT,
max_subid_length: 250,
max_filters_per_req: DEFAULT_MAX_FILTERS_PER_REQ,
max_filter_limit: 500,
max_subscription_bytes: DEFAULT_MAX_SUBSCRIPTION_BYTES,
max_negentropy_subscriptions: 10,
max_negentropy_items: DEFAULT_MAX_NEGENTROPY_ITEMS,
max_filter_limit: Some(DEFAULT_MAX_QUERY_RESULTS),
max_query_results: DEFAULT_MAX_QUERY_RESULTS,
default_filter_limit: 500,
auth_dm: false,
min_pow: None,
kinds_blacklist: HashSet::from(BLACKLISTED_KINDS),
Expand Down Expand Up @@ -483,13 +476,20 @@ impl LocalRelayBuilder {
self
}

/// Sets the maximum number of filters in one REQ. Defaults to 20.
/// Sets the maximum number of filters allowed in a REQ. Defaults to 20.
#[inline]
pub fn max_filters_per_req(mut self, max: usize) -> Self {
self.max_filters_per_req = max;
self
}

/// Sets the maximum limit for the filter. Defaults to 500.
#[inline]
pub fn max_filter_limit(mut self, max: usize) -> Self {
self.max_filter_limit = max;
self
}

/// Sets the maximum total bytes retained by active subscriptions per connection.
/// Defaults to 1 MiB.
#[inline]
Expand All @@ -514,27 +514,16 @@ impl LocalRelayBuilder {
self
}

/// Sets the maximum limit for the filter. If the filter's limit exceeds
/// this value, it will fallback to this number.
#[inline]
pub fn max_filter_limit(mut self, max: usize) -> Self {
self.max_filter_limit = Some(max);
/// This is a no-op
#[deprecated(note = "Use max_filters_per_req instead")]
pub fn max_query_results(self, _max: usize) -> Self {
self
}

/// Sets the maximum aggregate number of events returned by one REQ.
/// Defaults to 500.
#[inline]
pub fn max_query_results(mut self, max: usize) -> Self {
self.max_query_results = max;
self
}

/// Sets the default filter limit when no limit is specified. Defaults 500.
#[inline]
pub fn default_filter_limit(mut self, limit: usize) -> Self {
self.default_filter_limit = limit;
self
/// Sets the maximum limit for the filter. Defaults to 500.
#[deprecated(note = "Use max_filter_limit instead")]
pub fn default_filter_limit(self, limit: usize) -> Self {
self.max_filter_limit(limit)
}

/// If enabled, NIP-42 will be used for DMs, returning GiftWrap events for
Expand Down
119 changes: 36 additions & 83 deletions nostr-sdk/src/local_relay/local/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,10 @@ pub(super) struct InnerLocalRelay {
websocket_handshake_timeout: Duration,
max_subid_length: usize,
max_filters_per_req: usize,
max_filter_limit: usize,
max_subscription_bytes: usize,
max_negentropy_subscriptions: usize,
max_negentropy_items: usize,
max_filter_limit: Option<usize>,
max_query_results: usize,
default_filter_limit: usize,
auth_dm: bool,
min_pow: Option<u8>, // TODO: use AtomicU8 to allow to change it?
kinds_blacklist: HashSet<Kind>,
Expand Down Expand Up @@ -121,12 +119,10 @@ impl InnerLocalRelay {
websocket_handshake_timeout: builder.websocket_handshake_timeout,
max_subid_length: builder.max_subid_length,
max_filters_per_req: builder.max_filters_per_req,
max_filter_limit: builder.max_filter_limit,
max_subscription_bytes: builder.max_subscription_bytes,
max_negentropy_subscriptions: builder.max_negentropy_subscriptions,
max_negentropy_items: builder.max_negentropy_items,
max_filter_limit: builder.max_filter_limit,
max_query_results: builder.max_query_results,
default_filter_limit: builder.default_filter_limit,
auth_dm: builder.auth_dm,
min_pow: builder.min_pow,
kinds_blacklist: builder.kinds_blacklist,
Expand Down Expand Up @@ -1247,9 +1243,8 @@ impl InnerLocalRelay {
RelayMessage::Closed {
subscription_id,
message: Cow::Owned(format!(
"{}: REQ exceeds max filter count {}",
MachineReadablePrefix::RateLimited,
self.max_filters_per_req
"{}: too many filters",
MachineReadablePrefix::Blocked
)),
},
)
Expand Down Expand Up @@ -1316,18 +1311,36 @@ impl InnerLocalRelay {
}
}

// Cap each query and the merged result so multiple filters cannot multiply output.
for filter in filters.iter_mut() {
let requested_limit = filter.limit.unwrap_or_else(|| {
filter
.ids
.as_ref()
.map_or(self.default_filter_limit, |ids| ids.len())
});
let per_filter_limit = self
.max_filter_limit
.map_or(requested_limit, |max| requested_limit.min(max));
filter.limit = Some(per_filter_limit.min(self.max_query_results));
match filter.limit {
Some(filter_limit) => {
// If the limit is greater than the max limit, use the max limit
if filter_limit > self.max_filter_limit {
filter.limit = Some(self.max_filter_limit)
}
}
// No limit set, if the filter has IDs, set the limit to the number of IDs, otherwise to the default limit.
None => match filter.ids.as_ref() {
Some(ids) => {
if ids.len() > self.max_filter_limit {
return send_msg(
ws_tx,
RelayMessage::Closed {
subscription_id,
message: Cow::Owned(format!(
"{}: requested too many event IDs",
MachineReadablePrefix::Blocked
)),
},
)
.await;
}

filter.limit = Some(ids.len());
}
None => filter.limit = Some(self.max_filter_limit),
},
}
}

// Check if subscription has IDs
Expand All @@ -1342,13 +1355,11 @@ impl InnerLocalRelay {

let keys = Keys::generate();

for _ in 0..500.min(self.max_query_results) {
for _ in 0..500 {
events.insert(EventBuilder::new(Kind::TextNote, "Test").finalize(&keys)?);
}

events
} else if self.max_query_results == 0 {
BTreeSet::new()
} else {
let mut events: BTreeSet<Event> = BTreeSet::new();

Expand Down Expand Up @@ -1720,8 +1731,7 @@ mod tests {
assert_eq!(relay.auth_events_per_minute, 30);
assert_eq!(relay.messages_per_minute, 6_000);
assert_eq!(relay.max_filters_per_req, 20);
assert_eq!(relay.max_filter_limit, Some(500));
assert_eq!(relay.max_query_results, 500);
assert_eq!(relay.max_filter_limit, 500);
assert_eq!(relay.max_subscription_bytes, 1024 * 1024);
assert_eq!(relay.max_negentropy_items, 50_000);
assert_eq!(config.max_message_size, Some(5 * 1024 * 1024));
Expand Down Expand Up @@ -1926,7 +1936,7 @@ mod tests {
subscription_id,
message,
} if subscription_id.as_str() == "filters"
&& message == "rate-limited: REQ exceeds max filter count 1"
&& message == "blocked: too many filters"
));
}

Expand Down Expand Up @@ -2209,63 +2219,6 @@ mod tests {
));
}

#[tokio::test]
async fn req_results_are_bounded_and_streamed() {
let relay = InnerLocalRelay::new(
LocalRelayBuilder::default()
.max_filter_limit(1)
.max_query_results(1),
);
for content in ["one", "two"] {
let event = EventBuilder::new(Kind::TextNote, content)
.finalize(&Keys::generate())
.unwrap();
relay.database.save_event(&event).await.unwrap();
}

let (server_stream, client_stream) = tokio::io::duplex(16 * 1024);
let server = WebSocketStream::from_raw_socket(server_stream, Role::Server, None).await;
let mut client = WebSocketStream::from_raw_socket(client_stream, Role::Client, None).await;
let (mut server_tx, _) = server.split();
let mut session = session(None);
session.query_tokens = Tokens::new(1);
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);

relay
.handle_client_msg(
&mut session,
&mut server_tx,
ClientMessage::Req {
subscription_id: Cow::Owned(SubscriptionId::new("bounded")),
filters: vec![Cow::Owned(Filter::new().limit(usize::MAX))],
},
&addr,
32,
)
.await
.unwrap();

let event = client.next().await.unwrap().unwrap();
let eose = client.next().await.unwrap().unwrap();
assert!(matches!(
event,
Message::Text(json)
if matches!(
RelayMessage::from_json(json.as_bytes()).unwrap(),
RelayMessage::Event { .. }
)
));
assert!(matches!(
eose,
Message::Text(json)
if matches!(
RelayMessage::from_json(json.as_bytes()).unwrap(),
RelayMessage::EndOfStoredEvents(subscription_id)
if subscription_id.as_str() == "bounded"
)
));
}

#[test]
fn gift_wrap_auth_restricts_filters_without_kinds() {
let relay = InnerLocalRelay::new(LocalRelayBuilder::default().auth_dm(true));
Expand Down
Loading
Loading