commit 9721af957951dce0e785de5cc4878c136965ba2e
parent 39b8b42d3268f8f1c35ecf0406af9979023c9be5
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 21:22:30 -0700
runtime: make count exact
- split COUNT filtering from REQ default-limit injection
- add an exact-count Pocket query mode for accepted counts
- prove default and client limits do not cap accepted COUNT results
- extend benchmark coverage for exact bounded counts above default_limit
Diffstat:
3 files changed, 134 insertions(+), 12 deletions(-)
diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs
@@ -1169,15 +1169,27 @@ fn run_count_resource_control_benchmark(dataset: &BenchDataset) -> Result<Scenar
let operations = vec![
QueryOperation::new(
"bounded-public-count",
- filter_from_value(&json!({"kinds": [1], "#h": [public_group.id()], "limit": 25}))?,
+ filter_from_value(&json!({"kinds": [1], "#h": [public_group.id()]}))?,
QueryAuth::None,
- QueryExpectation::AtLeast(1),
+ QueryExpectation::Exactly(
+ dataset
+ .config
+ .public_events_per_group
+ .try_into()
+ .expect("public event count fits in u64"),
+ ),
),
QueryOperation::new(
"bounded-private-owner-count",
- filter_from_value(&json!({"kinds": [1], "#h": [private_group.id()], "limit": 25}))?,
+ filter_from_value(&json!({"kinds": [1], "#h": [private_group.id()]}))?,
QueryAuth::Owner,
- QueryExpectation::AtLeast(1),
+ QueryExpectation::Exactly(
+ dataset
+ .config
+ .private_events_per_group
+ .try_into()
+ .expect("private event count fits in u64"),
+ ),
),
];
let started = Instant::now();
@@ -2377,7 +2389,7 @@ mod tests {
#[test]
fn count_resource_controls_scenario_accepts_bounded_counts_and_refuses_broad_counts() {
let dataset =
- BenchDataset::generate(BenchDatasetConfig::new(3, 1, 1, 0, 1)).expect("dataset");
+ BenchDataset::generate(BenchDatasetConfig::new(3, 101, 101, 0, 1)).expect("dataset");
let scenario =
super::run_count_resource_control_benchmark(&dataset).expect("count controls");
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -214,6 +214,12 @@ impl BaseRelayCountEventsReport {
}
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum BaseRelayFilterLimitMode {
+ ApplyDefaultLimit,
+ PreserveCountLimitless,
+}
+
fn is_nip70_protected_event(event: &Event) -> bool {
event
.unsigned()
@@ -1157,7 +1163,13 @@ impl BaseRelay {
let mut query_metrics = BaseRelayQueryMetrics::default();
for filter in filters {
let report = Self::query_filter_events_report_with_services(
- store, groups, limits, query, filter, auth,
+ store,
+ groups,
+ limits,
+ query,
+ filter,
+ auth,
+ BaseRelayFilterLimitMode::ApplyDefaultLimit,
)?;
group_read_denied |= report.group_read_denied;
query_metrics = query_metrics.add(report.query_metrics);
@@ -1185,10 +1197,17 @@ impl BaseRelay {
let mut seen = BTreeSet::new();
let mut group_read_denied = false;
let mut query_metrics = BaseRelayQueryMetrics::default();
+ let count_query = query.exact_count();
for filter in filters {
let filter = filter.without_limit();
let report = Self::query_filter_events_report_with_services(
- store, groups, limits, query, &filter, auth,
+ store,
+ groups,
+ limits,
+ count_query,
+ &filter,
+ auth,
+ BaseRelayFilterLimitMode::PreserveCountLimitless,
)?;
group_read_denied |= report.group_read_denied;
query_metrics = query_metrics.add(report.query_metrics);
@@ -1212,8 +1231,9 @@ impl BaseRelay {
query: PocketQueryConfig,
filter: &Filter,
auth: &GroupAuthContext,
+ limit_mode: BaseRelayFilterLimitMode,
) -> Result<BaseRelayEventQueryReport, BaseRelayError> {
- let effective_filter = Self::filter_with_limits(limits, filter);
+ let effective_filter = Self::filter_with_limit_mode(limits, filter, limit_mode);
let pocket_filter = tangle_filter_to_pocket(&effective_filter)?;
let screen_error = RefCell::new(None);
let candidates_scanned = Cell::new(0_u64);
@@ -1260,10 +1280,16 @@ impl BaseRelay {
))
}
- fn filter_with_limits(limits: BaseRelayLimits, filter: &Filter) -> Filter {
- match filter.limit() {
- Some(_) => filter.clone(),
- None => filter.with_limit(limits.default_limit()),
+ fn filter_with_limit_mode(
+ limits: BaseRelayLimits,
+ filter: &Filter,
+ limit_mode: BaseRelayFilterLimitMode,
+ ) -> Filter {
+ match (limit_mode, filter.limit()) {
+ (BaseRelayFilterLimitMode::ApplyDefaultLimit, None) => {
+ filter.with_limit(limits.default_limit())
+ }
+ _ => filter.clone(),
}
}
@@ -1936,6 +1962,73 @@ mod tests {
}
#[test]
+ fn base_relay_count_does_not_apply_default_or_client_limits() {
+ let config = test_store_config("base-relay-count-no-default-limit");
+ let relay = BaseRelay::open(
+ &config,
+ BaseRelayLimits::new(BaseRelayLimitSettings {
+ max_pending_events: 4,
+ max_subscription_id_length: 64,
+ max_subscriptions: 64,
+ max_filters_per_request: 10,
+ max_tag_values_per_filter: 10,
+ max_query_complexity: 4,
+ max_event_tags: 200,
+ max_content_length: 65_536,
+ max_limit: 10,
+ default_limit: 1,
+ })
+ .expect("limits"),
+ PocketQueryConfig::default(),
+ )
+ .expect("relay");
+ let first = signed_event_at(7, 1, Vec::new(), "first", 1_714_124_433);
+ let second = signed_event_at(7, 1, Vec::new(), "second", 1_714_124_434);
+ let third = signed_event_at(7, 1, Vec::new(), "third", 1_714_124_435);
+
+ for event in [&first, &second, &third] {
+ assert_accepted(relay.handle_event(event.clone()).expect("event"), event);
+ }
+
+ let unbounded = filter_from_value(&serde_json::json!({
+ "authors": [first.unsigned().pubkey().as_str()],
+ "kinds": [1]
+ }))
+ .expect("unbounded");
+ let client_limited = filter_from_value(&serde_json::json!({
+ "authors": [first.unsigned().pubkey().as_str()],
+ "kinds": [1],
+ "limit": 1
+ }))
+ .expect("client limited");
+
+ assert_eq!(
+ relay
+ .handle_count(
+ SubscriptionId::new("count-unbounded").expect("sub"),
+ vec![unbounded]
+ )
+ .expect("count"),
+ RelayMessage::Count {
+ subscription_id: SubscriptionId::new("count-unbounded").expect("sub"),
+ count: 3
+ }
+ );
+ assert_eq!(
+ relay
+ .handle_count(
+ SubscriptionId::new("count-client-limited").expect("sub"),
+ vec![client_limited]
+ )
+ .expect("count"),
+ RelayMessage::Count {
+ subscription_id: SubscriptionId::new("count-client-limited").expect("sub"),
+ count: 3
+ }
+ );
+ }
+
+ #[test]
fn base_relay_event_path_rejects_invalid_signatures_and_skips_ephemeral_storage() {
let relay = test_relay("base-relay-event-store-path", 8);
let valid = signed_public_event(7, 1, Vec::new(), "valid");
diff --git a/crates/tangle_store_pocket/src/lib.rs b/crates/tangle_store_pocket/src/lib.rs
@@ -124,6 +124,14 @@ impl PocketQueryConfig {
pub fn allow_scrape_if_max_seconds(self) -> u64 {
self.allow_scrape_if_max_seconds
}
+
+ pub fn exact_count(self) -> Self {
+ Self::new(
+ true,
+ self.allow_scrape_if_limited_to,
+ self.allow_scrape_if_max_seconds,
+ )
+ }
}
impl Default for PocketQueryConfig {
@@ -602,6 +610,15 @@ mod tests {
}
#[test]
+ fn pocket_query_config_exact_count_enables_scrape_scan() {
+ let config = PocketQueryConfig::new(false, 7, 11).exact_count();
+
+ assert!(config.allow_scraping());
+ assert_eq!(config.allow_scrape_if_limited_to(), 7);
+ assert_eq!(config.allow_scrape_if_max_seconds(), 11);
+ }
+
+ #[test]
fn pocket_store_handle_opens_syncs_and_exposes_tangle_tables() {
let root = std::env::temp_dir().join(format!("tangle-pocket-store-{}", std::process::id()));
let config = PocketStoreConfig::new(root.join("pocket"), PocketSyncPolicy::FlushOnShutdown)