tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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:
Mcrates/tangle_bench/src/lib.rs | 22+++++++++++++++++-----
Mcrates/tangle_runtime/src/relay/core.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/tangle_store_pocket/src/lib.rs | 17+++++++++++++++++
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)