1use crate::network_service::{self, BroadcastStatementResult};
19use alloc::{string::String, vec::Vec};
20use core::{num::NonZero, time::Duration};
21use smoldot::json_rpc::methods::{
22 HexString, InternalError, InvalidReason, StatementSubmitResult, TopicFilter,
23};
24use smoldot::network::codec;
25
26#[derive(Debug, Clone)]
28pub struct StatementProtocolConfig {
29 max_seen_statements: NonZero<usize>,
31 false_positive_rate: f64,
32 bloom_seed: u128,
33 affinity_update_interval: Duration,
34}
35
36impl StatementProtocolConfig {
37 pub fn new(
38 max_seen_statements: NonZero<usize>,
39 false_positive_rate: f64,
40 bloom_seed: u128,
41 affinity_update_interval: Duration,
42 ) -> Self {
43 assert!(
44 false_positive_rate.is_finite()
45 && false_positive_rate > 0.0
46 && false_positive_rate < 1.0
47 );
48 assert!(!affinity_update_interval.is_zero());
49 StatementProtocolConfig {
50 max_seen_statements,
51 false_positive_rate,
52 bloom_seed,
53 affinity_update_interval,
54 }
55 }
56
57 pub fn max_seen_statements(&self) -> NonZero<usize> {
58 self.max_seen_statements
59 }
60
61 pub fn false_positive_rate(&self) -> f64 {
62 self.false_positive_rate
63 }
64
65 pub fn bloom_seed(&self) -> u128 {
66 self.bloom_seed
67 }
68
69 pub fn affinity_update_interval(&self) -> Duration {
70 self.affinity_update_interval
71 }
72}
73
74pub async fn validate_and_broadcast_statement<F, Fut>(
79 encoded: &[u8],
80 broadcast: F,
81) -> StatementSubmitResult
82where
83 F: FnOnce(Vec<u8>) -> Fut,
84 Fut: core::future::Future<Output = BroadcastStatementResult>,
85{
86 if codec::decode_statement(encoded).is_err() {
87 return StatementSubmitResult::Invalid {
88 reason: InvalidReason::Encoding,
89 };
90 }
91
92 let broadcasted = broadcast(encoded.to_vec()).await;
93 if broadcasted.total == 0 {
94 StatementSubmitResult::InternalError {
95 error: InternalError::NoConnectedPeers,
96 }
97 } else {
98 StatementSubmitResult::New
99 }
100}
101
102pub(super) struct StatementSubscription {
103 topic_filter: TopicFilter,
104 seen: Option<lru::LruCache<[u8; 32], (), fnv::FnvBuildHasher>>,
105}
106
107impl StatementSubscription {
108 pub(super) fn new(topic_filter: TopicFilter, max_seen: Option<NonZero<usize>>) -> Self {
109 Self {
110 topic_filter,
111 seen: max_seen
112 .map(|cap| lru::LruCache::with_hasher(cap, fnv::FnvBuildHasher::default())),
113 }
114 }
115
116 pub(super) fn accept(&mut self, hash: &[u8; 32], statement: &codec::Statement) -> bool {
117 if !self.topic_filter.matches(&statement.topics) {
118 return false;
119 }
120 if let Some(seen) = &mut self.seen {
121 if seen.put(*hash, ()).is_some() {
122 return false;
123 }
124 }
125 true
126 }
127}
128
129pub(super) struct StatementSubscriptions {
135 subscriptions: hashbrown::HashMap<String, StatementSubscription, fnv::FnvBuildHasher>,
137
138 by_topic: hashbrown::HashMap<
141 [u8; 32],
142 hashbrown::HashSet<String, fnv::FnvBuildHasher>,
143 fnv::FnvBuildHasher,
144 >,
145
146 wildcard: hashbrown::HashSet<String, fnv::FnvBuildHasher>,
149}
150
151impl StatementSubscriptions {
152 pub(super) fn with_capacity(capacity: usize) -> Self {
153 Self {
154 subscriptions: hashbrown::HashMap::with_capacity_and_hasher(
155 capacity,
156 Default::default(),
157 ),
158 by_topic: hashbrown::HashMap::with_hasher(Default::default()),
159 wildcard: hashbrown::HashSet::with_hasher(Default::default()),
160 }
161 }
162
163 pub(super) fn is_empty(&self) -> bool {
164 self.subscriptions.is_empty()
165 }
166
167 pub(super) fn insert(
169 &mut self,
170 id: String,
171 topic_filter: TopicFilter,
172 max_seen: Option<NonZero<usize>>,
173 ) {
174 match &topic_filter {
175 TopicFilter::Any => {
176 self.wildcard.insert(id.clone());
177 }
178 TopicFilter::MatchAll(topics) if topics.is_empty() => {
180 self.wildcard.insert(id.clone());
181 }
182 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
183 for topic in topics {
184 self.by_topic
185 .entry(*topic)
186 .or_insert_with(|| hashbrown::HashSet::with_hasher(Default::default()))
187 .insert(id.clone());
188 }
189 }
190 }
191
192 self.subscriptions
193 .insert(id, StatementSubscription::new(topic_filter, max_seen));
194 }
195
196 pub(super) fn remove(&mut self, id: &str) -> bool {
198 let Some(sub) = self.subscriptions.remove(id) else {
199 return false;
200 };
201
202 match &sub.topic_filter {
203 TopicFilter::Any => {
204 self.wildcard.remove(id);
205 }
206 TopicFilter::MatchAll(topics) if topics.is_empty() => {
207 self.wildcard.remove(id);
208 }
209 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
210 for topic in topics {
211 if let Some(ids) = self.by_topic.get_mut(topic) {
212 ids.remove(id);
213 if ids.is_empty() {
214 self.by_topic.remove(topic);
215 }
216 }
217 }
218 }
219 }
220
221 true
222 }
223
224 pub(super) fn shrink_to_fit(&mut self) {
225 self.subscriptions.shrink_to_fit();
226 for ids in self.by_topic.values_mut() {
227 ids.shrink_to_fit();
228 }
229 self.by_topic.shrink_to_fit();
230 self.wildcard.shrink_to_fit();
231 }
232
233 pub(super) fn matching(
240 &mut self,
241 statements: &[([u8; 32], codec::Statement)],
242 ) -> Vec<(String, Vec<HexString>)> {
243 let Self {
245 subscriptions,
246 by_topic,
247 wildcard,
248 } = self;
249
250 let mut out: hashbrown::HashMap<&str, Vec<HexString>, fnv::FnvBuildHasher> =
252 hashbrown::HashMap::with_hasher(Default::default());
253 let mut candidates: hashbrown::HashSet<&str, fnv::FnvBuildHasher> =
255 hashbrown::HashSet::with_hasher(Default::default());
256
257 for (hash, statement) in statements {
258 candidates.clear();
259 candidates.extend(wildcard.iter().map(String::as_str));
260 for topic in &statement.topics {
261 if let Some(ids) = by_topic.get(topic) {
262 candidates.extend(ids.iter().map(String::as_str));
263 }
264 }
265
266 let mut encoded: Option<HexString> = None;
268 for id in &candidates {
269 let sub = subscriptions
270 .get_mut(*id)
271 .expect("`candidates` is a subset of `subscriptions`; qed");
272 if sub.accept(hash, statement) {
273 let encoded = encoded.get_or_insert_with(|| {
274 HexString(
275 codec::encode_statement(statement)
276 .expect("re-encoding a decoded statement always succeeds; qed"),
277 )
278 });
279 out.entry(*id).or_default().push(encoded.clone());
280 }
281 }
282 }
283
284 out.into_iter()
285 .map(|(id, matching)| (String::from(id), matching))
286 .collect()
287 }
288
289 pub(super) fn build_combined_affinity_filter(
290 &self,
291 config: &StatementProtocolConfig,
292 ) -> network_service::AffinityFilter {
293 let mut all_topics: Vec<&[u8; 32]> = Vec::new();
294
295 for sub in self.subscriptions.values() {
296 match &sub.topic_filter {
297 TopicFilter::Any => {
298 return network_service::AffinityFilter::match_all(config.bloom_seed());
299 }
300 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
301 all_topics.extend(topics.iter());
302 }
303 }
304 }
305
306 network_service::AffinityFilter::from_topics(
307 all_topics.into_iter(),
308 config.bloom_seed(),
309 config.false_positive_rate(),
310 )
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use alloc::string::ToString as _;
318 use core::time::Duration;
319 use futures_lite::future::block_on;
320
321 const SEED: u128 = 0x5EED_5EED_5EED_5EED_5EED_5EED_5EED_5EED;
322 const FPR: f64 = 0.01;
323
324 fn test_config() -> StatementProtocolConfig {
325 StatementProtocolConfig::new(
326 NonZero::new(128).unwrap(),
327 FPR,
328 SEED,
329 Duration::from_secs(1),
330 )
331 }
332
333 fn make_subscriptions(
334 entries: Vec<(&str, TopicFilter, Option<NonZero<usize>>)>,
335 ) -> StatementSubscriptions {
336 let mut subs = StatementSubscriptions::with_capacity(entries.len());
337 for (id, filter, max_seen) in entries {
338 subs.insert(id.to_string(), filter, max_seen);
339 }
340 subs
341 }
342
343 fn statement_with_topics(topics: Vec<[u8; 32]>) -> codec::Statement {
344 codec::Statement {
345 proof: None,
346 decryption_key: None,
347 expiry: 42,
348 channel: None,
349 topics,
350 data: None,
351 }
352 }
353
354 fn valid_statement() -> Vec<u8> {
355 codec::encode_statement(&codec::Statement {
356 proof: None,
357 decryption_key: None,
358 expiry: 42,
359 channel: None,
360 topics: Vec::new(),
361 data: None,
362 })
363 .unwrap()
364 }
365
366 #[test]
367 fn validate_and_broadcast_invalid_encoding() {
368 let result = block_on(validate_and_broadcast_statement(&[0xff, 0xff], |_| async {
369 unreachable!()
370 }));
371 assert_eq!(
372 result,
373 StatementSubmitResult::Invalid {
374 reason: InvalidReason::Encoding
375 }
376 );
377 }
378
379 #[test]
380 fn validate_and_broadcast_no_peers() {
381 let result = block_on(validate_and_broadcast_statement(
382 &valid_statement(),
383 |_| async { BroadcastStatementResult { sent: 0, total: 0 } },
384 ));
385 assert_eq!(
386 result,
387 StatementSubmitResult::InternalError {
388 error: InternalError::NoConnectedPeers
389 }
390 );
391 }
392
393 #[test]
394 fn validate_and_broadcast_new() {
395 let result = block_on(validate_and_broadcast_statement(
396 &valid_statement(),
397 |_| async { BroadcastStatementResult { sent: 3, total: 5 } },
398 ));
399 assert_eq!(result, StatementSubmitResult::New);
400 }
401
402 #[test]
403 fn build_combined_affinity_empty_subscriptions() {
404 let config = test_config();
405 let subs = make_subscriptions(vec![]);
406 let filter = subs.build_combined_affinity_filter(&config);
407
408 assert!(!filter.contains(&[1u8; 32]));
410 let broadcast: &[&[u8; 32]] = &[];
412 assert!(filter.matches_statement(broadcast));
413 }
414
415 #[test]
416 fn build_combined_affinity_any_filter_matches_everything() {
417 let config = test_config();
418 let subs = make_subscriptions(vec![("s", TopicFilter::Any, None)]);
419 let filter = subs.build_combined_affinity_filter(&config);
420
421 assert!(filter.contains(&[1u8; 32]));
423 assert!(filter.contains(&[99u8; 32]));
424 let t = [7u8; 32];
425 assert!(filter.matches_statement(&[&t]));
426 }
427
428 #[test]
429 fn build_combined_affinity_match_any_union() {
430 let config = test_config();
431 let t1 = [1u8; 32];
432 let t2 = [2u8; 32];
433 let subs = make_subscriptions(vec![
434 ("a", TopicFilter::match_any(vec![t1]).unwrap(), None),
435 ("b", TopicFilter::match_any(vec![t2]).unwrap(), None),
436 ]);
437 let filter = subs.build_combined_affinity_filter(&config);
438
439 assert!(filter.contains(&t1));
440 assert!(filter.contains(&t2));
441 }
442
443 #[test]
444 fn accept_fresh_statement_passes() {
445 let t1 = [1u8; 32];
446 let mut sub =
447 StatementSubscription::new(TopicFilter::match_any(vec![t1]).unwrap(), NonZero::new(8));
448 let stmt = statement_with_topics(vec![t1]);
449 assert!(sub.accept(&[0xbb; 32], &stmt));
450 }
451
452 #[test]
453 fn accept_duplicate_returns_false() {
454 let mut sub = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
455 let stmt = statement_with_topics(vec![]);
456 let hash = [0xcc; 32];
457 assert!(sub.accept(&hash, &stmt));
458 assert!(!sub.accept(&hash, &stmt));
459 }
460
461 #[test]
462 fn accept_lru_eviction_allows_resubmit() {
463 let mut sub = StatementSubscription::new(TopicFilter::Any, NonZero::new(2));
464 let stmt = statement_with_topics(vec![]);
465 let h_a = [0xa; 32];
466 let h_b = [0xb; 32];
467 let h_c = [0xc; 32];
468
469 assert!(sub.accept(&h_a, &stmt));
470 assert!(sub.accept(&h_b, &stmt));
471 assert!(sub.accept(&h_c, &stmt));
473 assert!(sub.accept(&h_a, &stmt));
475 }
476
477 #[test]
478 fn dedup_is_per_subscription() {
479 let mut sub_a = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
480 let mut sub_b = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
481 let stmt = statement_with_topics(vec![]);
482 let hash = [0xee; 32];
483
484 assert!(sub_a.accept(&hash, &stmt));
485 assert!(!sub_a.accept(&hash, &stmt));
486 assert!(sub_b.accept(&hash, &stmt));
488 }
489
490 fn batch_entry(hash: u8, topics: Vec<[u8; 32]>) -> ([u8; 32], codec::Statement) {
492 ([hash; 32], statement_with_topics(topics))
493 }
494
495 fn matched_ids(matches: &[(String, Vec<HexString>)]) -> Vec<String> {
497 let mut ids: Vec<String> = matches.iter().map(|(id, _)| id.clone()).collect();
498 ids.sort();
499 ids
500 }
501
502 #[test]
503 fn matching_match_any_only_returns_relevant_subscriptions() {
504 let t1 = [1u8; 32];
505 let t2 = [2u8; 32];
506 let mut subs = make_subscriptions(vec![
507 ("a", TopicFilter::match_any(vec![t1]).unwrap(), None),
508 ("b", TopicFilter::match_any(vec![t2]).unwrap(), None),
509 ]);
510
511 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
513 assert_eq!(matched_ids(&matches), vec!["a".to_string()]);
514
515 let matches = subs.matching(&[batch_entry(0xbb, vec![[9u8; 32]])]);
517 assert!(matches.is_empty());
518 }
519
520 #[test]
521 fn matching_wildcard_filters_match_every_statement() {
522 let mut subs = make_subscriptions(vec![
524 ("any", TopicFilter::Any, None),
525 ("all", TopicFilter::match_all(vec![]).unwrap(), None),
526 ]);
527
528 let matches = subs.matching(&[batch_entry(0x01, vec![[7u8; 32]])]);
529 assert_eq!(
530 matched_ids(&matches),
531 vec!["all".to_string(), "any".to_string()]
532 );
533
534 let matches = subs.matching(&[batch_entry(0x02, vec![])]);
535 assert_eq!(
536 matched_ids(&matches),
537 vec!["all".to_string(), "any".to_string()]
538 );
539 }
540
541 #[test]
542 fn matching_match_all_requires_every_topic() {
543 let t1 = [1u8; 32];
544 let t2 = [2u8; 32];
545 let mut subs = make_subscriptions(vec![(
546 "all",
547 TopicFilter::match_all(vec![t1, t2]).unwrap(),
548 None,
549 )]);
550
551 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
554 assert!(matches.is_empty());
555
556 let matches = subs.matching(&[batch_entry(0xbb, vec![t1, t2])]);
558 assert_eq!(matched_ids(&matches), vec!["all".to_string()]);
559 }
560
561 #[test]
562 fn matching_empty_match_any_never_matches() {
563 let mut subs = make_subscriptions(vec![(
564 "none",
565 TopicFilter::match_any(vec![]).unwrap(),
566 None,
567 )]);
568
569 let matches = subs.matching(&[batch_entry(0x01, vec![[1u8; 32]])]);
570 assert!(matches.is_empty());
571 let matches = subs.matching(&[batch_entry(0x02, vec![])]);
572 assert!(matches.is_empty());
573 }
574
575 #[test]
576 fn matching_dedup_applies_across_batches() {
577 let t1 = [1u8; 32];
578 let mut subs = make_subscriptions(vec![(
579 "a",
580 TopicFilter::match_any(vec![t1]).unwrap(),
581 NonZero::new(8),
582 )]);
583
584 let entry = batch_entry(0xaa, vec![t1]);
585 let matches = subs.matching(&[entry.clone()]);
586 assert_eq!(matches.len(), 1);
587 assert_eq!(matches[0].1.len(), 1);
588
589 let matches = subs.matching(&[entry]);
591 assert!(matches.is_empty());
592 }
593
594 #[test]
595 fn matching_groups_multiple_statements_per_subscription() {
596 let t1 = [1u8; 32];
597 let mut subs =
598 make_subscriptions(vec![("a", TopicFilter::match_any(vec![t1]).unwrap(), None)]);
599
600 let matches = subs.matching(&[batch_entry(0x01, vec![t1]), batch_entry(0x02, vec![t1])]);
601 assert_eq!(matches.len(), 1);
602 assert_eq!(matches[0].0, "a");
603 assert_eq!(matches[0].1.len(), 2);
604 }
605
606 #[test]
607 fn remove_cleans_reverse_index() {
608 let t1 = [1u8; 32];
609 let mut subs =
610 make_subscriptions(vec![("a", TopicFilter::match_any(vec![t1]).unwrap(), None)]);
611
612 assert!(subs.remove("a"));
613 assert!(!subs.remove("a"));
614 assert!(subs.is_empty());
615 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
617 assert!(matches.is_empty());
618 }
619}