1use crate::network_service::{self, BroadcastStatementResult};
19use alloc::{format, string::String, vec::Vec};
20use core::{num::NonZero, time::Duration};
21use smoldot::json_rpc::methods::{HexString, InvalidReason, StatementSubmitResult, TopicFilter};
22use smoldot::json_rpc::parse;
23use smoldot::network::codec;
24
25#[derive(Debug, Clone)]
27pub struct StatementProtocolConfig {
28 max_seen_statements: NonZero<usize>,
30 false_positive_rate: f64,
31 bloom_seed: u128,
32 affinity_update_interval: Duration,
33}
34
35impl StatementProtocolConfig {
36 pub fn new(
37 max_seen_statements: NonZero<usize>,
38 false_positive_rate: f64,
39 bloom_seed: u128,
40 affinity_update_interval: Duration,
41 ) -> Self {
42 assert!(
43 false_positive_rate.is_finite()
44 && false_positive_rate > 0.0
45 && false_positive_rate < 1.0
46 );
47 assert!(!affinity_update_interval.is_zero());
48 StatementProtocolConfig {
49 max_seen_statements,
50 false_positive_rate,
51 bloom_seed,
52 affinity_update_interval,
53 }
54 }
55
56 pub fn max_seen_statements(&self) -> NonZero<usize> {
57 self.max_seen_statements
58 }
59
60 pub fn false_positive_rate(&self) -> f64 {
61 self.false_positive_rate
62 }
63
64 pub fn bloom_seed(&self) -> u128 {
65 self.bloom_seed
66 }
67
68 pub fn affinity_update_interval(&self) -> Duration {
69 self.affinity_update_interval
70 }
71}
72
73pub const STATEMENT_STORE_ERROR_CODE: i64 = 7001;
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum StatementSubmitError {
81 InvalidEncoding,
83 NotSent { connected: usize },
86}
87
88impl StatementSubmitError {
89 pub fn to_json_rpc_error(&self, request_id_json: &str) -> String {
95 let message = match self {
98 StatementSubmitError::InvalidEncoding => {
99 String::from("Statement store error: Error decoding statement")
100 }
101 StatementSubmitError::NotSent { connected: 0 } => String::from(
102 "Statement store error: No connected peers to broadcast the statement to",
103 ),
104 StatementSubmitError::NotSent { connected } => format!(
106 "Statement store error: none of the {connected} connected peers accepted the \
107 statement"
108 ),
109 };
110
111 parse::build_error_response(
112 request_id_json,
113 parse::ErrorResponse::ApplicationDefined(STATEMENT_STORE_ERROR_CODE, &message),
114 None,
115 )
116 }
117}
118
119pub async fn validate_and_broadcast_statement<F, Fut>(
127 encoded: &[u8],
128 now_from_unix_epoch: Duration,
129 broadcast: F,
130) -> Result<StatementSubmitResult, StatementSubmitError>
131where
132 F: FnOnce(Vec<u8>) -> Fut,
133 Fut: core::future::Future<Output = BroadcastStatementResult>,
134{
135 let Ok(statement) = codec::decode_statement(encoded) else {
136 return Err(StatementSubmitError::InvalidEncoding);
137 };
138
139 if now_from_unix_epoch.as_secs() >= statement.expiry >> 32 {
140 return Ok(StatementSubmitResult::Invalid(
141 InvalidReason::AlreadyExpired,
142 ));
143 }
144
145 if encoded.len() > codec::MAX_STATEMENT_SIZE {
146 return Ok(StatementSubmitResult::Invalid(
147 InvalidReason::EncodingTooLarge {
148 submitted_size: encoded.len(),
149 max_size: codec::MAX_STATEMENT_SIZE,
150 },
151 ));
152 }
153
154 if statement.proof.is_none() {
155 return Ok(StatementSubmitResult::Invalid(InvalidReason::NoProof));
156 }
157
158 let broadcasted = broadcast(encoded.to_vec()).await;
159 if broadcasted.sent == 0 {
160 return Err(StatementSubmitError::NotSent {
161 connected: broadcasted.total,
162 });
163 }
164
165 Ok(StatementSubmitResult::New)
166}
167
168pub(super) struct StatementSubscription {
169 topic_filter: TopicFilter,
170 seen: Option<lru::LruCache<[u8; 32], (), fnv::FnvBuildHasher>>,
171}
172
173impl StatementSubscription {
174 pub(super) fn new(topic_filter: TopicFilter, max_seen: Option<NonZero<usize>>) -> Self {
175 Self {
176 topic_filter,
177 seen: max_seen
178 .map(|cap| lru::LruCache::with_hasher(cap, fnv::FnvBuildHasher::default())),
179 }
180 }
181
182 pub(super) fn accept(&mut self, hash: &[u8; 32], statement: &codec::Statement) -> bool {
183 if !self.topic_filter.matches(&statement.topics) {
184 return false;
185 }
186 if let Some(seen) = &mut self.seen {
187 if seen.put(*hash, ()).is_some() {
188 return false;
189 }
190 }
191 true
192 }
193}
194
195pub(super) struct StatementSubscriptions {
201 subscriptions: hashbrown::HashMap<String, StatementSubscription, fnv::FnvBuildHasher>,
203
204 by_topic: hashbrown::HashMap<
207 [u8; 32],
208 hashbrown::HashSet<String, fnv::FnvBuildHasher>,
209 fnv::FnvBuildHasher,
210 >,
211
212 wildcard: hashbrown::HashSet<String, fnv::FnvBuildHasher>,
215}
216
217impl StatementSubscriptions {
218 pub(super) fn with_capacity(capacity: usize) -> Self {
219 Self {
220 subscriptions: hashbrown::HashMap::with_capacity_and_hasher(
221 capacity,
222 Default::default(),
223 ),
224 by_topic: hashbrown::HashMap::with_hasher(Default::default()),
225 wildcard: hashbrown::HashSet::with_hasher(Default::default()),
226 }
227 }
228
229 pub(super) fn is_empty(&self) -> bool {
230 self.subscriptions.is_empty()
231 }
232
233 pub(super) fn insert(
235 &mut self,
236 id: String,
237 topic_filter: TopicFilter,
238 max_seen: Option<NonZero<usize>>,
239 ) {
240 match &topic_filter {
241 TopicFilter::Any => {
242 self.wildcard.insert(id.clone());
243 }
244 TopicFilter::MatchAll(topics) if topics.is_empty() => {
246 self.wildcard.insert(id.clone());
247 }
248 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
249 for topic in topics {
250 self.by_topic
251 .entry(*topic)
252 .or_insert_with(|| hashbrown::HashSet::with_hasher(Default::default()))
253 .insert(id.clone());
254 }
255 }
256 }
257
258 self.subscriptions
259 .insert(id, StatementSubscription::new(topic_filter, max_seen));
260 }
261
262 pub(super) fn remove(&mut self, id: &str) -> bool {
264 let Some(sub) = self.subscriptions.remove(id) else {
265 return false;
266 };
267
268 match &sub.topic_filter {
269 TopicFilter::Any => {
270 self.wildcard.remove(id);
271 }
272 TopicFilter::MatchAll(topics) if topics.is_empty() => {
273 self.wildcard.remove(id);
274 }
275 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
276 for topic in topics {
277 if let Some(ids) = self.by_topic.get_mut(topic) {
278 ids.remove(id);
279 if ids.is_empty() {
280 self.by_topic.remove(topic);
281 }
282 }
283 }
284 }
285 }
286
287 true
288 }
289
290 pub(super) fn shrink_to_fit(&mut self) {
291 self.subscriptions.shrink_to_fit();
292 for ids in self.by_topic.values_mut() {
293 ids.shrink_to_fit();
294 }
295 self.by_topic.shrink_to_fit();
296 self.wildcard.shrink_to_fit();
297 }
298
299 pub(super) fn matching(
306 &mut self,
307 statements: &[([u8; 32], codec::Statement)],
308 ) -> Vec<(String, Vec<HexString>)> {
309 let Self {
311 subscriptions,
312 by_topic,
313 wildcard,
314 } = self;
315
316 let mut out: hashbrown::HashMap<&str, Vec<HexString>, fnv::FnvBuildHasher> =
318 hashbrown::HashMap::with_hasher(Default::default());
319 let mut candidates: hashbrown::HashSet<&str, fnv::FnvBuildHasher> =
321 hashbrown::HashSet::with_hasher(Default::default());
322
323 for (hash, statement) in statements {
324 candidates.clear();
325 candidates.extend(wildcard.iter().map(String::as_str));
326 for topic in &statement.topics {
327 if let Some(ids) = by_topic.get(topic) {
328 candidates.extend(ids.iter().map(String::as_str));
329 }
330 }
331
332 let mut encoded: Option<HexString> = None;
334 for id in &candidates {
335 let sub = subscriptions
336 .get_mut(*id)
337 .expect("`candidates` is a subset of `subscriptions`; qed");
338 if sub.accept(hash, statement) {
339 let encoded = encoded.get_or_insert_with(|| {
340 HexString(
341 codec::encode_statement(statement)
342 .expect("re-encoding a decoded statement always succeeds; qed"),
343 )
344 });
345 out.entry(*id).or_default().push(encoded.clone());
346 }
347 }
348 }
349
350 out.into_iter()
351 .map(|(id, matching)| (String::from(id), matching))
352 .collect()
353 }
354
355 pub(super) fn build_combined_affinity_filter(
356 &self,
357 config: &StatementProtocolConfig,
358 ) -> network_service::AffinityFilter {
359 let mut all_topics: Vec<&[u8; 32]> = Vec::new();
360
361 for sub in self.subscriptions.values() {
362 match &sub.topic_filter {
363 TopicFilter::Any => {
364 return network_service::AffinityFilter::match_all(config.bloom_seed());
365 }
366 TopicFilter::MatchAll(topics) | TopicFilter::MatchAny(topics) => {
367 all_topics.extend(topics.iter());
368 }
369 }
370 }
371
372 network_service::AffinityFilter::from_topics(
373 all_topics.into_iter(),
374 config.bloom_seed(),
375 config.false_positive_rate(),
376 )
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use alloc::string::ToString as _;
384 use core::time::Duration;
385 use futures_lite::future::block_on;
386
387 const SEED: u128 = 0x5EED_5EED_5EED_5EED_5EED_5EED_5EED_5EED;
388 const FPR: f64 = 0.01;
389
390 fn test_config() -> StatementProtocolConfig {
391 StatementProtocolConfig::new(
392 NonZero::new(128).unwrap(),
393 FPR,
394 SEED,
395 Duration::from_secs(1),
396 )
397 }
398
399 fn make_subscriptions(
400 entries: Vec<(&str, TopicFilter, Option<NonZero<usize>>)>,
401 ) -> StatementSubscriptions {
402 let mut subs = StatementSubscriptions::with_capacity(entries.len());
403 for (id, filter, max_seen) in entries {
404 subs.insert(id.to_string(), filter, max_seen);
405 }
406 subs
407 }
408
409 fn statement_with_topics(topics: Vec<[u8; 32]>) -> codec::Statement {
410 codec::Statement {
411 proof: None,
412 decryption_key: None,
413 expiry: 42,
414 channel: None,
415 topics,
416 data: None,
417 }
418 }
419
420 const NOW: Duration = Duration::from_secs(1_000);
421
422 const FUTURE_EXPIRY: u64 = 2_000 << 32;
424
425 fn encoded_statement(with_proof: bool, expiry: u64, data: Option<Vec<u8>>) -> Vec<u8> {
426 codec::encode_statement(&codec::Statement {
427 proof: with_proof.then(|| codec::Proof::Ed25519 {
428 signature: [0; 64],
429 signer: [0; 32],
430 }),
431 decryption_key: None,
432 expiry,
433 channel: None,
434 topics: Vec::new(),
435 data,
436 })
437 .unwrap()
438 }
439
440 #[test]
441 fn validate_and_broadcast_invalid_encoding() {
442 let result = block_on(validate_and_broadcast_statement(
443 &[0xff, 0xff],
444 NOW,
445 |_| async { unreachable!() },
446 ));
447 assert_eq!(result, Err(StatementSubmitError::InvalidEncoding));
448 }
449
450 #[test]
451 fn validate_and_broadcast_already_expired() {
452 let encoded = encoded_statement(false, 500 << 32, None);
454 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
455 unreachable!()
456 }));
457 assert_eq!(
458 result,
459 Ok(StatementSubmitResult::Invalid(
460 InvalidReason::AlreadyExpired
461 ))
462 );
463 }
464
465 #[test]
466 fn validate_and_broadcast_expiry_equal_to_now_is_expired() {
467 let encoded = encoded_statement(true, NOW.as_secs() << 32, None);
468 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
469 unreachable!()
470 }));
471 assert_eq!(
472 result,
473 Ok(StatementSubmitResult::Invalid(
474 InvalidReason::AlreadyExpired
475 ))
476 );
477 }
478
479 #[test]
480 fn validate_and_broadcast_encoding_too_large() {
481 let encoded = encoded_statement(false, FUTURE_EXPIRY, Some(vec![0; 1024 * 1024]));
483 assert!(encoded.len() > codec::MAX_STATEMENT_SIZE);
484 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
485 unreachable!()
486 }));
487 assert_eq!(
488 result,
489 Ok(StatementSubmitResult::Invalid(
490 InvalidReason::EncodingTooLarge {
491 submitted_size: encoded.len(),
492 max_size: codec::MAX_STATEMENT_SIZE,
493 }
494 ))
495 );
496 }
497
498 #[test]
499 fn validate_and_broadcast_no_proof() {
500 let encoded = encoded_statement(false, FUTURE_EXPIRY, None);
501 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
502 unreachable!()
503 }));
504 assert_eq!(
505 result,
506 Ok(StatementSubmitResult::Invalid(InvalidReason::NoProof))
507 );
508 }
509
510 #[test]
511 fn validate_and_broadcast_no_peers() {
512 let encoded = encoded_statement(true, FUTURE_EXPIRY, None);
513 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
514 BroadcastStatementResult { sent: 0, total: 0 }
515 }));
516 assert_eq!(result, Err(StatementSubmitError::NotSent { connected: 0 }));
517 }
518
519 #[test]
520 fn submit_errors_carry_the_statement_store_code() {
521 assert_eq!(
524 StatementSubmitError::InvalidEncoding.to_json_rpc_error("7"),
525 r#"{"jsonrpc":"2.0","id":7,"error":{"code":7001,"message":"Statement store error: Error decoding statement"}}"#
526 );
527 assert_eq!(
528 StatementSubmitError::NotSent { connected: 0 }.to_json_rpc_error("7"),
529 r#"{"jsonrpc":"2.0","id":7,"error":{"code":7001,"message":"Statement store error: No connected peers to broadcast the statement to"}}"#
530 );
531 assert_eq!(
533 StatementSubmitError::NotSent { connected: 5 }.to_json_rpc_error("7"),
534 r#"{"jsonrpc":"2.0","id":7,"error":{"code":7001,"message":"Statement store error: none of the 5 connected peers accepted the statement"}}"#
535 );
536 }
537
538 #[test]
539 fn validate_and_broadcast_reaching_no_peer_is_not_new() {
540 let encoded = encoded_statement(true, FUTURE_EXPIRY, None);
543 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
544 BroadcastStatementResult { sent: 0, total: 5 }
545 }));
546 assert_eq!(result, Err(StatementSubmitError::NotSent { connected: 5 }));
547 }
548
549 #[test]
550 fn validate_and_broadcast_new() {
551 let encoded = encoded_statement(true, FUTURE_EXPIRY, None);
552 let result = block_on(validate_and_broadcast_statement(&encoded, NOW, |_| async {
553 BroadcastStatementResult { sent: 3, total: 5 }
554 }));
555 assert_eq!(result, Ok(StatementSubmitResult::New));
556 }
557
558 #[test]
559 fn build_combined_affinity_empty_subscriptions() {
560 let config = test_config();
561 let subs = make_subscriptions(vec![]);
562 let filter = subs.build_combined_affinity_filter(&config);
563
564 assert!(!filter.contains(&[1u8; 32]));
566 let broadcast: &[&[u8; 32]] = &[];
568 assert!(filter.matches_statement(broadcast));
569 }
570
571 #[test]
572 fn build_combined_affinity_any_filter_matches_everything() {
573 let config = test_config();
574 let subs = make_subscriptions(vec![("s", TopicFilter::Any, None)]);
575 let filter = subs.build_combined_affinity_filter(&config);
576
577 assert!(filter.contains(&[1u8; 32]));
579 assert!(filter.contains(&[99u8; 32]));
580 let t = [7u8; 32];
581 assert!(filter.matches_statement(&[&t]));
582 }
583
584 #[test]
585 fn build_combined_affinity_match_any_union() {
586 let config = test_config();
587 let t1 = [1u8; 32];
588 let t2 = [2u8; 32];
589 let subs = make_subscriptions(vec![
590 ("a", TopicFilter::match_any(vec![t1]).unwrap(), None),
591 ("b", TopicFilter::match_any(vec![t2]).unwrap(), None),
592 ]);
593 let filter = subs.build_combined_affinity_filter(&config);
594
595 assert!(filter.contains(&t1));
596 assert!(filter.contains(&t2));
597 }
598
599 #[test]
600 fn accept_fresh_statement_passes() {
601 let t1 = [1u8; 32];
602 let mut sub =
603 StatementSubscription::new(TopicFilter::match_any(vec![t1]).unwrap(), NonZero::new(8));
604 let stmt = statement_with_topics(vec![t1]);
605 assert!(sub.accept(&[0xbb; 32], &stmt));
606 }
607
608 #[test]
609 fn accept_duplicate_returns_false() {
610 let mut sub = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
611 let stmt = statement_with_topics(vec![]);
612 let hash = [0xcc; 32];
613 assert!(sub.accept(&hash, &stmt));
614 assert!(!sub.accept(&hash, &stmt));
615 }
616
617 #[test]
618 fn accept_lru_eviction_allows_resubmit() {
619 let mut sub = StatementSubscription::new(TopicFilter::Any, NonZero::new(2));
620 let stmt = statement_with_topics(vec![]);
621 let h_a = [0xa; 32];
622 let h_b = [0xb; 32];
623 let h_c = [0xc; 32];
624
625 assert!(sub.accept(&h_a, &stmt));
626 assert!(sub.accept(&h_b, &stmt));
627 assert!(sub.accept(&h_c, &stmt));
629 assert!(sub.accept(&h_a, &stmt));
631 }
632
633 #[test]
634 fn dedup_is_per_subscription() {
635 let mut sub_a = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
636 let mut sub_b = StatementSubscription::new(TopicFilter::Any, NonZero::new(8));
637 let stmt = statement_with_topics(vec![]);
638 let hash = [0xee; 32];
639
640 assert!(sub_a.accept(&hash, &stmt));
641 assert!(!sub_a.accept(&hash, &stmt));
642 assert!(sub_b.accept(&hash, &stmt));
644 }
645
646 fn batch_entry(hash: u8, topics: Vec<[u8; 32]>) -> ([u8; 32], codec::Statement) {
648 ([hash; 32], statement_with_topics(topics))
649 }
650
651 fn matched_ids(matches: &[(String, Vec<HexString>)]) -> Vec<String> {
653 let mut ids: Vec<String> = matches.iter().map(|(id, _)| id.clone()).collect();
654 ids.sort();
655 ids
656 }
657
658 #[test]
659 fn matching_match_any_only_returns_relevant_subscriptions() {
660 let t1 = [1u8; 32];
661 let t2 = [2u8; 32];
662 let mut subs = make_subscriptions(vec![
663 ("a", TopicFilter::match_any(vec![t1]).unwrap(), None),
664 ("b", TopicFilter::match_any(vec![t2]).unwrap(), None),
665 ]);
666
667 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
669 assert_eq!(matched_ids(&matches), vec!["a".to_string()]);
670
671 let matches = subs.matching(&[batch_entry(0xbb, vec![[9u8; 32]])]);
673 assert!(matches.is_empty());
674 }
675
676 #[test]
677 fn matching_wildcard_filters_match_every_statement() {
678 let mut subs = make_subscriptions(vec![
680 ("any", TopicFilter::Any, None),
681 ("all", TopicFilter::match_all(vec![]).unwrap(), None),
682 ]);
683
684 let matches = subs.matching(&[batch_entry(0x01, vec![[7u8; 32]])]);
685 assert_eq!(
686 matched_ids(&matches),
687 vec!["all".to_string(), "any".to_string()]
688 );
689
690 let matches = subs.matching(&[batch_entry(0x02, vec![])]);
691 assert_eq!(
692 matched_ids(&matches),
693 vec!["all".to_string(), "any".to_string()]
694 );
695 }
696
697 #[test]
698 fn matching_match_all_requires_every_topic() {
699 let t1 = [1u8; 32];
700 let t2 = [2u8; 32];
701 let mut subs = make_subscriptions(vec![(
702 "all",
703 TopicFilter::match_all(vec![t1, t2]).unwrap(),
704 None,
705 )]);
706
707 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
710 assert!(matches.is_empty());
711
712 let matches = subs.matching(&[batch_entry(0xbb, vec![t1, t2])]);
714 assert_eq!(matched_ids(&matches), vec!["all".to_string()]);
715 }
716
717 #[test]
718 fn matching_empty_match_any_never_matches() {
719 let mut subs = make_subscriptions(vec![(
720 "none",
721 TopicFilter::match_any(vec![]).unwrap(),
722 None,
723 )]);
724
725 let matches = subs.matching(&[batch_entry(0x01, vec![[1u8; 32]])]);
726 assert!(matches.is_empty());
727 let matches = subs.matching(&[batch_entry(0x02, vec![])]);
728 assert!(matches.is_empty());
729 }
730
731 #[test]
732 fn matching_dedup_applies_across_batches() {
733 let t1 = [1u8; 32];
734 let mut subs = make_subscriptions(vec![(
735 "a",
736 TopicFilter::match_any(vec![t1]).unwrap(),
737 NonZero::new(8),
738 )]);
739
740 let entry = batch_entry(0xaa, vec![t1]);
741 let matches = subs.matching(&[entry.clone()]);
742 assert_eq!(matches.len(), 1);
743 assert_eq!(matches[0].1.len(), 1);
744
745 let matches = subs.matching(&[entry]);
747 assert!(matches.is_empty());
748 }
749
750 #[test]
751 fn matching_groups_multiple_statements_per_subscription() {
752 let t1 = [1u8; 32];
753 let mut subs =
754 make_subscriptions(vec![("a", TopicFilter::match_any(vec![t1]).unwrap(), None)]);
755
756 let matches = subs.matching(&[batch_entry(0x01, vec![t1]), batch_entry(0x02, vec![t1])]);
757 assert_eq!(matches.len(), 1);
758 assert_eq!(matches[0].0, "a");
759 assert_eq!(matches[0].1.len(), 2);
760 }
761
762 #[test]
763 fn remove_cleans_reverse_index() {
764 let t1 = [1u8; 32];
765 let mut subs =
766 make_subscriptions(vec![("a", TopicFilter::match_any(vec![t1]).unwrap(), None)]);
767
768 assert!(subs.remove("a"));
769 assert!(!subs.remove("a"));
770 assert!(subs.is_empty());
771 let matches = subs.matching(&[batch_entry(0xaa, vec![t1])]);
773 assert!(matches.is_empty());
774 }
775}