lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit e778aaa15590a7c3c335d3a6756797fc55883cd6
parent 148daafbdc0aa10fb9cdd8e32a34b2681e0b4a98
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 08:45:23 +0000

transport: seal fetched event aggregates

- make fetch-only aggregate fields private behind bounded accessors
- bind accepted receipt order to unique fetched event identities
- derive relay summaries and counters from authenticated evidence
- enforce aggregate raw JSON receipt and diagnostic ceilings

Diffstat:
Mcrates/transport_nostr/src/fetch.rs | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 207 insertions(+), 17 deletions(-)

diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs @@ -801,6 +801,21 @@ impl RadrootsRelayFetchEventReceipt { pub fn message(&self) -> Option<&str> { self.message.as_deref() } + + fn is_fetch_accepted(&self) -> bool { + self.event_id.is_some() + && !self.inserted + && !self.duplicate + && !self.not_persisted + && !self.malformed + && !self.out_of_filter + && !self.skipped_over_limit + && self.verification == RadrootsRelayFetchEventVerification::Verified + && self.admission == RadrootsRelayFetchEventAdmission::NotEvaluated + && self.admission_code.is_none() + && self.valid_stream == RadrootsRelayFetchEventValidStream::NotEvaluated + && self.visibility == RadrootsRelayFetchEventVisibility::NotEvaluated + } } #[derive(Deserialize)] @@ -1006,24 +1021,198 @@ fn invalid_fetch_receipt( #[derive(Clone, Debug)] pub struct RadrootsRelayFetchedEventsReceipt { - pub target_relays: Vec<String>, - pub connected_relays: Vec<String>, - pub failed_relays: Vec<RadrootsRelayFetchFailure>, - pub events: Vec<RadrootsRelayFetchedEvent>, - pub event_receipts: Vec<RadrootsRelayFetchEventReceipt>, - pub duplicate_count: usize, - pub verification_failed_count: usize, - pub malformed_count: usize, - pub out_of_filter_count: usize, - pub skipped_over_limit_count: usize, - pub eose_count: usize, - pub truncated_count: usize, - pub closed_count: usize, - pub notice_count: usize, - pub relay_outcomes: Vec<RadrootsRelayFetchRelayOutcome>, + target_relays: Vec<String>, + connected_relays: Vec<String>, + failed_relays: Vec<RadrootsRelayFetchFailure>, + events: Vec<RadrootsRelayFetchedEvent>, + event_receipts: Vec<RadrootsRelayFetchEventReceipt>, + duplicate_count: usize, + verification_failed_count: usize, + malformed_count: usize, + out_of_filter_count: usize, + skipped_over_limit_count: usize, + eose_count: usize, + truncated_count: usize, + closed_count: usize, + notice_count: usize, + relay_outcomes: Vec<RadrootsRelayFetchRelayOutcome>, } impl RadrootsRelayFetchedEventsReceipt { + fn checked(mut self) -> Result<Self, RadrootsRelayTransportError> { + self.target_relays = canonical_fetch_receipt_targets(self.target_relays)?; + if self.events.len() > RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX { + return Err(RadrootsRelayTransportError::FetchLimitTooLarge { + field: "fetched_event_count", + max: RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX, + actual: self.events.len(), + }); + } + let raw_receipt_count = self + .event_receipts + .len() + .checked_add(self.relay_outcomes.len()) + .ok_or_else(|| { + invalid_fetch_receipt("raw_receipt_count", "raw receipt count overflowed") + })?; + if raw_receipt_count > RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX { + return Err(RadrootsRelayTransportError::FetchLimitTooLarge { + field: "raw_receipt_count", + max: RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX, + actual: raw_receipt_count, + }); + } + + let mut aggregate_raw_json_bytes = 0usize; + let mut fetched_event_ids = Vec::with_capacity(self.events.len()); + let mut seen_event_ids = BTreeSet::new(); + for event in &self.events { + ensure_fetch_receipt_relay_requested(&self.target_relays, event.relay_url())?; + aggregate_raw_json_bytes = aggregate_raw_json_bytes + .checked_add(event.raw_json().len()) + .ok_or_else(|| { + invalid_fetch_receipt("aggregate_raw_json_bytes", "byte count overflowed") + })?; + if aggregate_raw_json_bytes > RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX { + return Err(RadrootsRelayTransportError::FetchLimitTooLarge { + field: "aggregate_raw_json_bytes", + max: RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX, + actual: aggregate_raw_json_bytes, + }); + } + let event_id = event.event().id.to_hex(); + if !seen_event_ids.insert(event_id.clone()) { + return Err(invalid_fetch_receipt( + "events", + format!("duplicate fetched event id `{event_id}`"), + )); + } + fetched_event_ids.push(event_id); + } + for event_receipt in &self.event_receipts { + event_receipt.clone().checked()?; + ensure_fetch_receipt_relay_requested(&self.target_relays, event_receipt.relay_url())?; + } + let accepted_receipt_ids = self + .event_receipts + .iter() + .filter(|receipt| receipt.is_fetch_accepted()) + .filter_map(RadrootsRelayFetchEventReceipt::event_id) + .map(str::to_owned) + .collect::<Vec<_>>(); + if accepted_receipt_ids != fetched_event_ids { + return Err(invalid_fetch_receipt( + "events", + "accepted event receipts do not bind the exact fetched event sequence", + )); + } + + validate_fetch_relay_outcomes(&self.target_relays, &self.relay_outcomes)?; + validate_fetch_diagnostic_budget(&self.event_receipts, &self.relay_outcomes)?; + let expected_connected = self + .relay_outcomes + .iter() + .filter(|outcome| outcome.kind() == RadrootsRelayFetchOutcomeKind::Eose) + .map(|outcome| outcome.relay_url().to_owned()) + .collect::<Vec<_>>(); + if self.connected_relays != expected_connected { + return Err(invalid_fetch_receipt( + "connected_relays", + "connected relays must exactly match EOSE outcomes in wire order", + )); + } + let expected_failed = self + .relay_outcomes + .iter() + .filter(|outcome| outcome.kind() == RadrootsRelayFetchOutcomeKind::Closed) + .map(|outcome| RadrootsRelayFetchFailure { + relay_url: outcome.relay_url().to_owned(), + reason: outcome.message().unwrap_or_default().to_owned(), + }) + .collect::<Vec<_>>(); + if self.failed_relays != expected_failed { + return Err(invalid_fetch_receipt( + "failed_relays", + "failed relays must exactly match closed outcomes in wire order", + )); + } + + ensure_fetch_receipt_count( + "duplicate_count", + self.duplicate_count, + self.event_receipts + .iter() + .filter(|receipt| receipt.was_duplicate()) + .count(), + )?; + ensure_fetch_receipt_count( + "verification_failed_count", + self.verification_failed_count, + self.event_receipts + .iter() + .filter(|receipt| { + receipt.verification() == RadrootsRelayFetchEventVerification::Failed + }) + .count(), + )?; + ensure_fetch_receipt_count( + "malformed_count", + self.malformed_count, + self.event_receipts + .iter() + .filter(|receipt| receipt.is_malformed()) + .count(), + )?; + ensure_fetch_receipt_count( + "out_of_filter_count", + self.out_of_filter_count, + self.event_receipts + .iter() + .filter(|receipt| receipt.is_out_of_filter()) + .count(), + )?; + ensure_fetch_receipt_count( + "skipped_over_limit_count", + self.skipped_over_limit_count, + self.event_receipts + .iter() + .filter(|receipt| receipt.was_skipped_over_limit()) + .count(), + )?; + for (field, supplied, kind) in [ + ( + "eose_count", + self.eose_count, + RadrootsRelayFetchOutcomeKind::Eose, + ), + ( + "truncated_count", + self.truncated_count, + RadrootsRelayFetchOutcomeKind::Truncated, + ), + ( + "closed_count", + self.closed_count, + RadrootsRelayFetchOutcomeKind::Closed, + ), + ( + "notice_count", + self.notice_count, + RadrootsRelayFetchOutcomeKind::Notice, + ), + ] { + ensure_fetch_receipt_count( + field, + supplied, + self.relay_outcomes + .iter() + .filter(|outcome| outcome.kind() == kind) + .count(), + )?; + } + Ok(self) + } + pub fn target_relays(&self) -> &[String] { &self.target_relays } @@ -1378,7 +1567,7 @@ impl RadrootsRelayProcessedFetch { reason: outcome.message.clone().unwrap_or_default(), }) .collect(); - Ok(RadrootsRelayFetchedEventsReceipt { + RadrootsRelayFetchedEventsReceipt { target_relays: self.target_relays, connected_relays, failed_relays, @@ -1394,7 +1583,8 @@ impl RadrootsRelayProcessedFetch { closed_count: self.closed_count, notice_count: self.notice_count, relay_outcomes: self.relay_outcomes, - }) + } + .checked() } }