1use dashmap::DashMap;
24use libp2p::PeerId;
25use parking_lot::RwLock;
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28use std::sync::Arc;
29use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
30use thiserror::Error;
31
32#[derive(Error, Debug)]
34pub enum GossipSubError {
35 #[error("Topic not found: {0}")]
36 TopicNotFound(String),
37
38 #[error("Already subscribed to topic: {0}")]
39 AlreadySubscribed(String),
40
41 #[error("Not subscribed to topic: {0}")]
42 NotSubscribed(String),
43
44 #[error("Message too large: {size} bytes (max: {max})")]
45 MessageTooLarge { size: usize, max: usize },
46
47 #[error("Invalid topic name: {0}")]
48 InvalidTopicName(String),
49
50 #[error("Peer scoring error: {0}")]
51 ScoringError(String),
52}
53
54#[derive(Debug, Clone)]
56pub struct GossipSubConfig {
57 pub mesh_n_low: usize,
59
60 pub mesh_n: usize,
62
63 pub mesh_n_high: usize,
65
66 pub gossip_n: usize,
68
69 pub heartbeat_interval: Duration,
71
72 pub max_message_size: usize,
74
75 pub enable_scoring: bool,
77
78 pub duplicate_cache_time: Duration,
80
81 pub max_duplicate_cache_size: usize,
83
84 pub enable_validation: bool,
86}
87
88impl Default for GossipSubConfig {
89 fn default() -> Self {
90 Self {
91 mesh_n_low: 4,
92 mesh_n: 6,
93 mesh_n_high: 12,
94 gossip_n: 3,
95 heartbeat_interval: Duration::from_secs(1),
96 max_message_size: 1024 * 1024, enable_scoring: true,
98 duplicate_cache_time: Duration::from_secs(120),
99 max_duplicate_cache_size: 10000,
100 enable_validation: true,
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
107pub struct TopicId(pub String);
108
109impl TopicId {
110 pub fn new(name: impl Into<String>) -> Self {
111 Self(name.into())
112 }
113
114 pub fn content_announce() -> Self {
116 Self("/ipfrs/content/announce/1.0.0".to_string())
117 }
118
119 pub fn peer_announce() -> Self {
121 Self("/ipfrs/peer/announce/1.0.0".to_string())
122 }
123
124 pub fn dht_events() -> Self {
126 Self("/ipfrs/dht/events/1.0.0".to_string())
127 }
128}
129
130#[derive(Debug, Clone)]
132pub struct GossipSubMessage {
133 pub id: MessageId,
135
136 pub source: PeerId,
138
139 pub topic: TopicId,
141
142 pub data: Vec<u8>,
144
145 pub sequence: u64,
147
148 pub timestamp: Instant,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
154pub struct MessageId(pub Vec<u8>);
155
156impl MessageId {
157 pub fn new(source: &PeerId, sequence: u64) -> Self {
159 let mut data = source.to_bytes();
160 data.extend_from_slice(&sequence.to_le_bytes());
161 Self(data)
162 }
163}
164
165#[derive(Debug, Clone, Default)]
167pub struct PeerScore {
168 pub topic_scores: HashMap<TopicId, f64>,
170
171 pub total_score: f64,
173
174 pub invalid_messages: u64,
176
177 pub valid_messages: u64,
179
180 pub last_update: Option<Instant>,
182}
183
184impl PeerScore {
185 pub fn calculate_total(&mut self) {
187 if self.topic_scores.is_empty() {
188 self.total_score = 0.0;
189 return;
190 }
191
192 let sum: f64 = self.topic_scores.values().sum();
194 self.total_score = sum / self.topic_scores.len() as f64;
195
196 let total_messages = self.invalid_messages + self.valid_messages;
198 if total_messages > 0 {
199 let invalid_ratio = self.invalid_messages as f64 / total_messages as f64;
200 self.total_score *= 1.0 - invalid_ratio;
201 }
202
203 self.last_update = Some(Instant::now());
204 }
205
206 pub fn update_topic_score(&mut self, topic: TopicId, score: f64) {
208 self.topic_scores.insert(topic, score);
209 self.calculate_total();
210 }
211
212 pub fn record_message(&mut self, valid: bool) {
214 if valid {
215 self.valid_messages += 1;
216 } else {
217 self.invalid_messages += 1;
218 }
219 self.calculate_total();
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct TopicSubscription {
226 pub topic: TopicId,
228
229 pub subscribed_at: Instant,
231
232 pub mesh_peers: HashSet<PeerId>,
234
235 pub messages_received: u64,
237
238 pub messages_published: u64,
240}
241
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct GossipSubStats {
245 pub subscribed_topics: usize,
247
248 pub messages_published: u64,
250
251 pub messages_received: u64,
253
254 pub duplicate_messages: u64,
256
257 pub invalid_messages: u64,
259
260 pub active_mesh_peers: usize,
262
263 pub mesh_prune_count: u64,
265
266 pub mesh_graft_count: u64,
268
269 pub messages_per_topic: HashMap<String, u64>,
271}
272
273#[derive(Debug, Clone)]
275struct SeenCacheEntry {
276 timestamp: Instant,
277}
278
279pub struct GossipSubManager {
281 config: GossipSubConfig,
283
284 subscriptions: Arc<DashMap<TopicId, TopicSubscription>>,
286
287 peer_scores: Arc<DashMap<PeerId, PeerScore>>,
289
290 seen_messages: Arc<DashMap<MessageId, SeenCacheEntry>>,
292
293 sequence_counter: Arc<RwLock<u64>>,
295
296 stats: Arc<RwLock<GossipSubStats>>,
298}
299
300impl GossipSubManager {
301 pub fn new(config: GossipSubConfig) -> Self {
303 Self {
304 config,
305 subscriptions: Arc::new(DashMap::new()),
306 peer_scores: Arc::new(DashMap::new()),
307 seen_messages: Arc::new(DashMap::new()),
308 sequence_counter: Arc::new(RwLock::new(0)),
309 stats: Arc::new(RwLock::new(GossipSubStats::default())),
310 }
311 }
312
313 pub fn subscribe(&self, topic: TopicId) -> Result<(), GossipSubError> {
315 if self.subscriptions.contains_key(&topic) {
316 return Err(GossipSubError::AlreadySubscribed(topic.0.clone()));
317 }
318
319 let subscription = TopicSubscription {
320 topic: topic.clone(),
321 subscribed_at: Instant::now(),
322 mesh_peers: HashSet::new(),
323 messages_received: 0,
324 messages_published: 0,
325 };
326
327 self.subscriptions.insert(topic.clone(), subscription);
328
329 let mut stats = self.stats.write();
330 stats.subscribed_topics = self.subscriptions.len();
331
332 Ok(())
333 }
334
335 pub fn unsubscribe(&self, topic: &TopicId) -> Result<(), GossipSubError> {
337 self.subscriptions
338 .remove(topic)
339 .ok_or_else(|| GossipSubError::NotSubscribed(topic.0.clone()))?;
340
341 let mut stats = self.stats.write();
342 stats.subscribed_topics = self.subscriptions.len();
343
344 Ok(())
345 }
346
347 pub fn publish(
349 &self,
350 topic: TopicId,
351 data: Vec<u8>,
352 source: PeerId,
353 ) -> Result<MessageId, GossipSubError> {
354 if !self.subscriptions.contains_key(&topic) {
356 return Err(GossipSubError::NotSubscribed(topic.0.clone()));
357 }
358
359 if data.len() > self.config.max_message_size {
361 return Err(GossipSubError::MessageTooLarge {
362 size: data.len(),
363 max: self.config.max_message_size,
364 });
365 }
366
367 let sequence = {
369 let mut counter = self.sequence_counter.write();
370 *counter += 1;
371 *counter
372 };
373
374 let message_id = MessageId::new(&source, sequence);
376
377 if let Some(mut subscription) = self.subscriptions.get_mut(&topic) {
379 subscription.messages_published += 1;
380 }
381
382 let mut stats = self.stats.write();
383 stats.messages_published += 1;
384 *stats.messages_per_topic.entry(topic.0.clone()).or_insert(0) += 1;
385
386 Ok(message_id)
387 }
388
389 pub fn handle_message(&self, message: GossipSubMessage) -> Result<bool, GossipSubError> {
391 if self.is_duplicate(&message.id) {
393 let mut stats = self.stats.write();
394 stats.duplicate_messages += 1;
395 return Ok(false); }
397
398 self.add_to_seen_cache(message.id.clone());
400
401 if self.config.enable_validation && !self.validate_message(&message) {
403 let mut stats = self.stats.write();
404 stats.invalid_messages += 1;
405
406 if self.config.enable_scoring {
408 if let Some(mut score) = self.peer_scores.get_mut(&message.source) {
409 score.record_message(false);
410 }
411 }
412
413 return Ok(false);
414 }
415
416 if let Some(mut subscription) = self.subscriptions.get_mut(&message.topic) {
418 subscription.messages_received += 1;
419 }
420
421 let mut stats = self.stats.write();
422 stats.messages_received += 1;
423
424 if self.config.enable_scoring {
426 self.peer_scores
427 .entry(message.source)
428 .or_default()
429 .record_message(true);
430 }
431
432 Ok(true) }
434
435 fn is_duplicate(&self, message_id: &MessageId) -> bool {
437 if let Some(entry) = self.seen_messages.get(message_id) {
438 let age = Instant::now().duration_since(entry.timestamp);
439 return age < self.config.duplicate_cache_time;
440 }
441 false
442 }
443
444 fn add_to_seen_cache(&self, message_id: MessageId) {
446 let entry = SeenCacheEntry {
447 timestamp: Instant::now(),
448 };
449
450 self.seen_messages.insert(message_id, entry);
451
452 if self.seen_messages.len() > self.config.max_duplicate_cache_size {
454 self.cleanup_seen_cache();
455 }
456 }
457
458 fn cleanup_seen_cache(&self) {
460 let now = Instant::now();
461 let ttl = self.config.duplicate_cache_time;
462
463 self.seen_messages
464 .retain(|_, entry| now.duration_since(entry.timestamp) < ttl);
465 }
466
467 fn validate_message(&self, _message: &GossipSubMessage) -> bool {
469 true
472 }
473
474 pub fn add_peer_to_mesh(&self, topic: &TopicId, peer: PeerId) -> Result<(), GossipSubError> {
476 let inserted = {
477 let mut subscription = self
478 .subscriptions
479 .get_mut(topic)
480 .ok_or_else(|| GossipSubError::NotSubscribed(topic.0.clone()))?;
481 subscription.mesh_peers.insert(peer)
482 }; if inserted {
485 let mut stats = self.stats.write();
486 stats.mesh_graft_count += 1;
487 stats.active_mesh_peers = self.count_mesh_peers();
488 }
489
490 Ok(())
491 }
492
493 pub fn remove_peer_from_mesh(
495 &self,
496 topic: &TopicId,
497 peer: &PeerId,
498 ) -> Result<(), GossipSubError> {
499 let removed = {
500 let mut subscription = self
501 .subscriptions
502 .get_mut(topic)
503 .ok_or_else(|| GossipSubError::NotSubscribed(topic.0.clone()))?;
504 subscription.mesh_peers.remove(peer)
505 }; if removed {
508 let mut stats = self.stats.write();
509 stats.mesh_prune_count += 1;
510 stats.active_mesh_peers = self.count_mesh_peers();
511 }
512
513 Ok(())
514 }
515
516 pub fn get_mesh_peers(&self, topic: &TopicId) -> Result<Vec<PeerId>, GossipSubError> {
518 let subscription = self
519 .subscriptions
520 .get(topic)
521 .ok_or_else(|| GossipSubError::NotSubscribed(topic.0.clone()))?;
522
523 Ok(subscription.mesh_peers.iter().cloned().collect())
524 }
525
526 fn count_mesh_peers(&self) -> usize {
528 self.subscriptions
529 .iter()
530 .map(|entry| entry.mesh_peers.len())
531 .sum()
532 }
533
534 pub fn get_peer_score(&self, peer: &PeerId) -> Option<PeerScore> {
536 self.peer_scores.get(peer).map(|s| s.clone())
537 }
538
539 pub fn update_peer_score(&self, peer: &PeerId, topic: TopicId, score: f64) {
541 self.peer_scores
542 .entry(*peer)
543 .or_default()
544 .update_topic_score(topic, score);
545 }
546
547 pub fn get_peers_to_prune(&self, topic: &TopicId, threshold: f64) -> Vec<PeerId> {
549 let subscription = match self.subscriptions.get(topic) {
550 Some(sub) => sub,
551 None => return Vec::new(),
552 };
553
554 subscription
555 .mesh_peers
556 .iter()
557 .filter(|peer| {
558 if let Some(score) = self.peer_scores.get(peer) {
559 score.total_score < threshold
560 } else {
561 false
562 }
563 })
564 .cloned()
565 .collect()
566 }
567
568 pub fn stats(&self) -> GossipSubStats {
570 self.stats.read().clone()
571 }
572
573 pub fn list_topics(&self) -> Vec<TopicId> {
575 self.subscriptions
576 .iter()
577 .map(|entry| entry.key().clone())
578 .collect()
579 }
580
581 pub fn is_subscribed(&self, topic: &TopicId) -> bool {
583 self.subscriptions.contains_key(topic)
584 }
585}
586
587pub mod topics {
589 pub const INFERENCE_REQUEST: &str = "/ipfrs/inference/request/1.0.0";
591 pub const INFERENCE_RESULT: &str = "/ipfrs/inference/result/1.0.0";
593 pub const PROVIDER_ANNOUNCE: &str = "/ipfrs/provider/announce/1.0.0";
595 pub const BLOCK_ANNOUNCE: &str = "/ipfrs/block/announce/1.0.0";
597 pub const GRADIENT_SYNC: &str = "/ipfrs/gradient/sync/1.0.0";
599 pub const KB_DELTA: &str = "/ipfrs/kb/delta/1.0.0";
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct TopicMessage {
606 pub topic: String,
607 pub sender_peer_id: String,
608 pub payload: Vec<u8>,
609 pub timestamp: u64,
610 pub sequence: u64,
611}
612
613impl TopicMessage {
614 pub fn new(topic: &str, sender: &str, payload: Vec<u8>) -> Self {
616 let timestamp = SystemTime::now()
617 .duration_since(UNIX_EPOCH)
618 .unwrap_or_default()
619 .as_secs();
620 Self {
621 topic: topic.to_string(),
622 sender_peer_id: sender.to_string(),
623 payload,
624 timestamp,
625 sequence: 0,
626 }
627 }
628
629 pub fn encode(&self) -> Result<Vec<u8>, serde_json::Error> {
631 serde_json::to_vec(self)
632 }
633
634 pub fn decode(bytes: &[u8]) -> Result<Self, serde_json::Error> {
636 serde_json::from_slice(bytes)
637 }
638}
639
640pub struct IpfrsTopic {
642 pub name: String,
643 pub hash: libp2p::gossipsub::TopicHash,
644}
645
646impl IpfrsTopic {
647 pub fn new(name: &str) -> Self {
648 use libp2p::gossipsub::IdentTopic;
649 let topic = IdentTopic::new(name);
650 IpfrsTopic {
651 name: name.to_string(),
652 hash: topic.hash(),
653 }
654 }
655}
656
657impl GossipSubManager {
658 pub fn subscribe_inference_topics(&self) -> Result<(), GossipSubError> {
660 use topics::{
661 BLOCK_ANNOUNCE, GRADIENT_SYNC, INFERENCE_REQUEST, INFERENCE_RESULT, KB_DELTA,
662 PROVIDER_ANNOUNCE,
663 };
664 let all_topics = [
665 INFERENCE_REQUEST,
666 INFERENCE_RESULT,
667 PROVIDER_ANNOUNCE,
668 BLOCK_ANNOUNCE,
669 GRADIENT_SYNC,
670 KB_DELTA,
671 ];
672 for t in &all_topics {
673 let tid = TopicId::new(*t);
674 match self.subscribe(tid) {
676 Ok(()) | Err(GossipSubError::AlreadySubscribed(_)) => {}
677 Err(e) => return Err(e),
678 }
679 }
680 Ok(())
681 }
682
683 pub fn publish_inference_request(
685 &self,
686 request_bytes: &[u8],
687 sender_peer_id: &str,
688 ) -> Result<(), GossipSubError> {
689 let msg = TopicMessage::new(
690 topics::INFERENCE_REQUEST,
691 sender_peer_id,
692 request_bytes.to_vec(),
693 );
694 let encoded = msg
695 .encode()
696 .map_err(|e| GossipSubError::InvalidTopicName(format!("encode error: {}", e)))?;
697 let topic = TopicId::new(topics::INFERENCE_REQUEST);
698 let source = self.dummy_local_peer();
701 self.publish(topic, encoded, source)?;
702 Ok(())
703 }
704
705 pub fn publish_inference_result(
707 &self,
708 result_bytes: &[u8],
709 sender_peer_id: &str,
710 ) -> Result<(), GossipSubError> {
711 let msg = TopicMessage::new(
712 topics::INFERENCE_RESULT,
713 sender_peer_id,
714 result_bytes.to_vec(),
715 );
716 let encoded = msg
717 .encode()
718 .map_err(|e| GossipSubError::InvalidTopicName(format!("encode error: {}", e)))?;
719 let topic = TopicId::new(topics::INFERENCE_RESULT);
720 let source = self.dummy_local_peer();
721 self.publish(topic, encoded, source)?;
722 Ok(())
723 }
724
725 pub fn announce_block(&self, cid: &str, sender_peer_id: &str) -> Result<(), GossipSubError> {
727 let msg = TopicMessage::new(
728 topics::BLOCK_ANNOUNCE,
729 sender_peer_id,
730 cid.as_bytes().to_vec(),
731 );
732 let encoded = msg
733 .encode()
734 .map_err(|e| GossipSubError::InvalidTopicName(format!("encode error: {}", e)))?;
735 let topic = TopicId::new(topics::BLOCK_ANNOUNCE);
736 let source = self.dummy_local_peer();
737 self.publish(topic, encoded, source)?;
738 Ok(())
739 }
740
741 pub fn publish_gradient_cid(
746 &self,
747 cid: &str,
748 sender_peer_id: &str,
749 ) -> Result<(), GossipSubError> {
750 let msg = TopicMessage::new(
751 topics::GRADIENT_SYNC,
752 sender_peer_id,
753 cid.as_bytes().to_vec(),
754 );
755 let encoded = msg
756 .encode()
757 .map_err(|e| GossipSubError::InvalidTopicName(format!("encode error: {e}")))?;
758 let topic = TopicId::new(topics::GRADIENT_SYNC);
759 let source = self.dummy_local_peer();
760 self.publish(topic, encoded, source)?;
761 Ok(())
762 }
763
764 pub fn announce_provider(&self, cid: &str, peer_id: &str) -> Result<(), GossipSubError> {
766 let msg = TopicMessage::new(topics::PROVIDER_ANNOUNCE, peer_id, cid.as_bytes().to_vec());
767 let encoded = msg
768 .encode()
769 .map_err(|e| GossipSubError::InvalidTopicName(format!("encode error: {}", e)))?;
770 let topic = TopicId::new(topics::PROVIDER_ANNOUNCE);
771 let source = self.dummy_local_peer();
772 self.publish(topic, encoded, source)?;
773 Ok(())
774 }
775
776 fn dummy_local_peer(&self) -> PeerId {
780 use libp2p::identity::Keypair;
781 let seed = [0u8; 32];
782 Keypair::ed25519_from_bytes(seed.to_vec())
784 .map(|kp| kp.public().to_peer_id())
785 .unwrap_or_else(|_| PeerId::random())
786 }
787}
788
789#[cfg(test)]
790mod topic_tests {
791 use super::*;
792 use topics::{
793 BLOCK_ANNOUNCE, GRADIENT_SYNC, INFERENCE_REQUEST, INFERENCE_RESULT, KB_DELTA,
794 PROVIDER_ANNOUNCE,
795 };
796
797 #[test]
798 fn test_topic_message_roundtrip() {
799 let original =
800 TopicMessage::new(INFERENCE_REQUEST, "peer-abc", b"hello inference".to_vec());
801 let encoded = original.encode().expect("encode should succeed");
802 let decoded = TopicMessage::decode(&encoded).expect("decode should succeed");
803 assert_eq!(decoded.topic, INFERENCE_REQUEST);
804 assert_eq!(decoded.sender_peer_id, "peer-abc");
805 assert_eq!(decoded.payload, b"hello inference");
806 }
807
808 #[test]
809 fn test_all_topic_constants_unique() {
810 let topics = [
811 INFERENCE_REQUEST,
812 INFERENCE_RESULT,
813 PROVIDER_ANNOUNCE,
814 BLOCK_ANNOUNCE,
815 GRADIENT_SYNC,
816 KB_DELTA,
817 ];
818 let unique: std::collections::HashSet<_> = topics.iter().collect();
819 assert_eq!(
820 unique.len(),
821 topics.len(),
822 "all topic constants must be unique"
823 );
824 }
825
826 #[test]
827 fn test_ipfrs_topic_hash() {
828 let t1 = IpfrsTopic::new(INFERENCE_REQUEST);
829 let t2 = IpfrsTopic::new(INFERENCE_REQUEST);
830 let t3 = IpfrsTopic::new(INFERENCE_RESULT);
831
832 assert_eq!(t1.name, INFERENCE_REQUEST);
833 assert_eq!(t1.hash, t2.hash, "same name must produce same hash");
834 assert_ne!(
835 t1.hash, t3.hash,
836 "different names must produce different hashes"
837 );
838 }
839
840 #[test]
841 fn test_subscribe_inference_topics_subscribes_all() {
842 let manager = GossipSubManager::new(GossipSubConfig::default());
843 manager
844 .subscribe_inference_topics()
845 .expect("should subscribe without error");
846
847 let all_topics = [
848 INFERENCE_REQUEST,
849 INFERENCE_RESULT,
850 PROVIDER_ANNOUNCE,
851 BLOCK_ANNOUNCE,
852 GRADIENT_SYNC,
853 KB_DELTA,
854 ];
855 for t in &all_topics {
856 assert!(
857 manager.is_subscribed(&TopicId::new(*t)),
858 "should be subscribed to {}",
859 t
860 );
861 }
862 }
863
864 #[test]
865 fn test_subscribe_inference_topics_idempotent() {
866 let manager = GossipSubManager::new(GossipSubConfig::default());
867 manager.subscribe_inference_topics().expect("first call ok");
868 manager
870 .subscribe_inference_topics()
871 .expect("second call should also be ok");
872 }
873
874 #[test]
875 fn test_publish_inference_request() {
876 let manager = GossipSubManager::new(GossipSubConfig::default());
877 manager.subscribe_inference_topics().expect("subscribe");
878 manager
879 .publish_inference_request(b"req_data", "peer-1")
880 .expect("publish inference request");
881 let stats = manager.stats();
882 assert!(stats.messages_published >= 1);
883 }
884
885 #[test]
886 fn test_publish_inference_result() {
887 let manager = GossipSubManager::new(GossipSubConfig::default());
888 manager.subscribe_inference_topics().expect("subscribe");
889 manager
890 .publish_inference_result(b"result_data", "peer-2")
891 .expect("publish inference result");
892 let stats = manager.stats();
893 assert!(stats.messages_published >= 1);
894 }
895
896 #[test]
897 fn test_announce_block() {
898 let manager = GossipSubManager::new(GossipSubConfig::default());
899 manager.subscribe_inference_topics().expect("subscribe");
900 manager
901 .announce_block("bafyreiabc123", "peer-3")
902 .expect("announce block");
903 let stats = manager.stats();
904 assert!(stats.messages_published >= 1);
905 }
906
907 #[test]
908 fn test_announce_provider() {
909 let manager = GossipSubManager::new(GossipSubConfig::default());
910 manager.subscribe_inference_topics().expect("subscribe");
911 manager
912 .announce_provider("bafyreiabc123", "peer-4")
913 .expect("announce provider");
914 let stats = manager.stats();
915 assert!(stats.messages_published >= 1);
916 }
917
918 #[test]
919 fn test_topic_message_timestamp_nonnegative() {
920 let msg = TopicMessage::new(KB_DELTA, "p", vec![1, 2, 3]);
921 assert!(
923 msg.timestamp > 1_577_836_800,
924 "timestamp should be after 2020"
925 );
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use libp2p::identity::Keypair;
933
934 fn test_peer_id(index: u8) -> PeerId {
936 let mut seed = [0u8; 32];
938 seed[0] = index;
939 let keypair = Keypair::ed25519_from_bytes(seed).expect("valid seed");
940 keypair.public().to_peer_id()
941 }
942
943 #[test]
944 fn test_gossipsub_manager_creation() {
945 let config = GossipSubConfig::default();
946 let manager = GossipSubManager::new(config);
947 assert_eq!(manager.list_topics().len(), 0);
948 }
949
950 #[test]
951 fn test_topic_subscription() {
952 let manager = GossipSubManager::new(GossipSubConfig::default());
953 let topic = TopicId::content_announce();
954
955 manager
956 .subscribe(topic.clone())
957 .expect("test: subscribe to content_announce topic");
958 assert!(manager.is_subscribed(&topic));
959 assert_eq!(manager.list_topics().len(), 1);
960 }
961
962 #[test]
963 fn test_duplicate_subscription() {
964 let manager = GossipSubManager::new(GossipSubConfig::default());
965 let topic = TopicId::content_announce();
966
967 manager
968 .subscribe(topic.clone())
969 .expect("test: first subscribe should succeed");
970 let result = manager.subscribe(topic);
971 assert!(matches!(result, Err(GossipSubError::AlreadySubscribed(_))));
972 }
973
974 #[test]
975 fn test_unsubscribe() {
976 let manager = GossipSubManager::new(GossipSubConfig::default());
977 let topic = TopicId::content_announce();
978
979 manager
980 .subscribe(topic.clone())
981 .expect("test: subscribe should succeed");
982 manager
983 .unsubscribe(&topic)
984 .expect("test: unsubscribe should succeed");
985 assert!(!manager.is_subscribed(&topic));
986 assert_eq!(manager.list_topics().len(), 0);
987 }
988
989 #[test]
990 fn test_publish_message() {
991 let manager = GossipSubManager::new(GossipSubConfig::default());
992 let topic = TopicId::content_announce();
993 let peer = test_peer_id(1);
994
995 manager
996 .subscribe(topic.clone())
997 .expect("test: subscribe should succeed");
998
999 let data = b"Hello, GossipSub!".to_vec();
1000 let message_id = manager
1001 .publish(topic, data, peer)
1002 .expect("test: publish should succeed");
1003
1004 let stats = manager.stats();
1005 assert_eq!(stats.messages_published, 1);
1006 assert!(!message_id.0.is_empty());
1007 }
1008
1009 #[test]
1010 fn test_publish_without_subscription() {
1011 let manager = GossipSubManager::new(GossipSubConfig::default());
1012 let topic = TopicId::content_announce();
1013 let peer = test_peer_id(1);
1014
1015 let data = b"Hello".to_vec();
1016 let result = manager.publish(topic, data, peer);
1017 assert!(matches!(result, Err(GossipSubError::NotSubscribed(_))));
1018 }
1019
1020 #[test]
1021 fn test_message_too_large() {
1022 let config = GossipSubConfig {
1023 max_message_size: 100,
1024 ..Default::default()
1025 };
1026 let manager = GossipSubManager::new(config);
1027 let topic = TopicId::content_announce();
1028 let peer = test_peer_id(1);
1029
1030 manager
1031 .subscribe(topic.clone())
1032 .expect("test: subscribe should succeed");
1033
1034 let data = vec![0u8; 200]; let result = manager.publish(topic, data, peer);
1036 assert!(matches!(
1037 result,
1038 Err(GossipSubError::MessageTooLarge { .. })
1039 ));
1040 }
1041
1042 #[test]
1043 fn test_message_deduplication() {
1044 let manager = GossipSubManager::new(GossipSubConfig::default());
1045 let topic = TopicId::content_announce();
1046 let peer = test_peer_id(1);
1047
1048 manager
1049 .subscribe(topic.clone())
1050 .expect("test: subscribe should succeed");
1051
1052 let message = GossipSubMessage {
1053 id: MessageId::new(&peer, 1),
1054 source: peer,
1055 topic: topic.clone(),
1056 data: b"Test".to_vec(),
1057 sequence: 1,
1058 timestamp: Instant::now(),
1059 };
1060
1061 let result1 = manager
1063 .handle_message(message.clone())
1064 .expect("test: handle first message should succeed");
1065 assert!(result1);
1066
1067 let result2 = manager
1069 .handle_message(message)
1070 .expect("test: handle duplicate message should succeed");
1071 assert!(!result2);
1072
1073 let stats = manager.stats();
1074 assert_eq!(stats.duplicate_messages, 1);
1075 }
1076
1077 #[test]
1078 fn test_peer_scoring() {
1079 let manager = GossipSubManager::new(GossipSubConfig::default());
1080 let peer = test_peer_id(1);
1081 let topic = TopicId::content_announce();
1082
1083 manager.update_peer_score(&peer, topic.clone(), 0.8);
1084
1085 let score = manager
1086 .get_peer_score(&peer)
1087 .expect("test: peer score should exist after update");
1088 assert_eq!(score.topic_scores.get(&topic), Some(&0.8));
1089 assert!(score.total_score > 0.0);
1090 }
1091
1092 #[test]
1093 fn test_mesh_management() {
1094 let manager = GossipSubManager::new(GossipSubConfig::default());
1095 let topic = TopicId::content_announce();
1096 let peer1 = test_peer_id(1);
1097 let peer2 = test_peer_id(2);
1098
1099 manager
1100 .subscribe(topic.clone())
1101 .expect("test: subscribe should succeed");
1102
1103 manager
1105 .add_peer_to_mesh(&topic, peer1)
1106 .expect("test: add peer1 to mesh should succeed");
1107 manager
1108 .add_peer_to_mesh(&topic, peer2)
1109 .expect("test: add peer2 to mesh should succeed");
1110
1111 let mesh_peers = manager
1112 .get_mesh_peers(&topic)
1113 .expect("test: get mesh peers should succeed");
1114 assert_eq!(mesh_peers.len(), 2);
1115 assert!(mesh_peers.contains(&peer1));
1116 assert!(mesh_peers.contains(&peer2));
1117
1118 manager
1120 .remove_peer_from_mesh(&topic, &peer1)
1121 .expect("test: remove peer1 from mesh should succeed");
1122 let mesh_peers = manager
1123 .get_mesh_peers(&topic)
1124 .expect("test: get mesh peers after removal should succeed");
1125 assert_eq!(mesh_peers.len(), 1);
1126 assert!(!mesh_peers.contains(&peer1));
1127 }
1128
1129 #[test]
1130 fn test_peers_to_prune() {
1131 let manager = GossipSubManager::new(GossipSubConfig::default());
1132 let topic = TopicId::content_announce();
1133 let peer1 = test_peer_id(1);
1134 let peer2 = test_peer_id(2);
1135
1136 manager
1137 .subscribe(topic.clone())
1138 .expect("test: subscribe should succeed");
1139 manager
1140 .add_peer_to_mesh(&topic, peer1)
1141 .expect("test: add peer1 to mesh should succeed");
1142 manager
1143 .add_peer_to_mesh(&topic, peer2)
1144 .expect("test: add peer2 to mesh should succeed");
1145
1146 manager.update_peer_score(&peer1, topic.clone(), 0.9);
1148 manager.update_peer_score(&peer2, topic.clone(), 0.3);
1149
1150 let to_prune = manager.get_peers_to_prune(&topic, 0.5);
1152 assert_eq!(to_prune.len(), 1);
1153 assert!(to_prune.contains(&peer2));
1154 }
1155
1156 #[test]
1157 fn test_topic_ids() {
1158 assert_eq!(
1159 TopicId::content_announce().0,
1160 "/ipfrs/content/announce/1.0.0"
1161 );
1162 assert_eq!(TopicId::peer_announce().0, "/ipfrs/peer/announce/1.0.0");
1163 assert_eq!(TopicId::dht_events().0, "/ipfrs/dht/events/1.0.0");
1164 }
1165
1166 #[test]
1167 fn test_message_id_generation() {
1168 let peer = test_peer_id(1);
1169 let id1 = MessageId::new(&peer, 1);
1170 let id2 = MessageId::new(&peer, 1);
1171 let id3 = MessageId::new(&peer, 2);
1172
1173 assert_eq!(id1, id2); assert_ne!(id1, id3); }
1176
1177 #[test]
1178 fn test_peer_score_calculation() {
1179 let mut score = PeerScore::default();
1180
1181 score.update_topic_score(TopicId::content_announce(), 0.8);
1182 score.update_topic_score(TopicId::peer_announce(), 0.6);
1183
1184 assert_eq!(score.topic_scores.len(), 2);
1185 assert_eq!(score.total_score, 0.7); score.record_message(false);
1189 assert!(score.total_score < 0.7); }
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq)]
1199pub enum MeshHealthStatus {
1200 Healthy,
1202 Underpeered {
1204 current: usize,
1206 minimum: usize,
1208 },
1209 Overloaded {
1211 current: usize,
1213 maximum: usize,
1215 },
1216}
1217
1218pub struct MeshHealthMonitor {
1225 d_low: usize,
1227 d_high: usize,
1229 heal_interval_secs: u64,
1231 last_heal_attempt: Option<Instant>,
1233 heal_attempts: u64,
1235}
1236
1237impl MeshHealthMonitor {
1238 pub fn new(d_low: usize, d_high: usize) -> Self {
1242 Self {
1243 d_low,
1244 d_high,
1245 heal_interval_secs: 30,
1246 last_heal_attempt: None,
1247 heal_attempts: 0,
1248 }
1249 }
1250
1251 pub fn with_heal_interval(mut self, secs: u64) -> Self {
1253 self.heal_interval_secs = secs;
1254 self
1255 }
1256
1257 pub fn d_low(&self) -> usize {
1259 self.d_low
1260 }
1261
1262 pub fn d_high(&self) -> usize {
1264 self.d_high
1265 }
1266
1267 pub fn heal_interval_secs(&self) -> u64 {
1269 self.heal_interval_secs
1270 }
1271
1272 pub fn heal_attempts(&self) -> u64 {
1274 self.heal_attempts
1275 }
1276
1277 pub fn is_mesh_underpeered(&self, topic_peer_count: usize) -> bool {
1279 topic_peer_count < self.d_low
1280 }
1281
1282 pub fn is_mesh_overloaded(&self, topic_peer_count: usize) -> bool {
1284 topic_peer_count > self.d_high
1285 }
1286
1287 pub fn health_status(&self, mesh_size: usize) -> MeshHealthStatus {
1289 if mesh_size < self.d_low {
1290 MeshHealthStatus::Underpeered {
1291 current: mesh_size,
1292 minimum: self.d_low,
1293 }
1294 } else if mesh_size > self.d_high {
1295 MeshHealthStatus::Overloaded {
1296 current: mesh_size,
1297 maximum: self.d_high,
1298 }
1299 } else {
1300 MeshHealthStatus::Healthy
1301 }
1302 }
1303
1304 pub fn suggest_grafts(&self, gossip_peers: &[String], mesh_peers: &[String]) -> Vec<String> {
1311 let mesh_set: std::collections::HashSet<&str> =
1312 mesh_peers.iter().map(|s| s.as_str()).collect();
1313
1314 gossip_peers
1315 .iter()
1316 .filter(|p| !mesh_set.contains(p.as_str()))
1317 .cloned()
1318 .collect()
1319 }
1320
1321 pub fn record_heal_attempt(&mut self) {
1326 self.last_heal_attempt = Some(Instant::now());
1327 self.heal_attempts += 1;
1328 }
1329
1330 pub fn should_attempt_heal(&self) -> bool {
1334 match self.last_heal_attempt {
1335 None => true,
1336 Some(last) => last.elapsed() >= Duration::from_secs(self.heal_interval_secs),
1337 }
1338 }
1339}
1340
1341impl GossipSubManager {
1346 pub fn mesh_health(&self, topic: &TopicId) -> MeshHealthStatus {
1350 let monitor = MeshHealthMonitor::new(self.config.mesh_n_low, self.config.mesh_n_high);
1351 let mesh_size = self
1352 .subscriptions
1353 .get(topic)
1354 .map(|s| s.mesh_peers.len())
1355 .unwrap_or(0);
1356 monitor.health_status(mesh_size)
1357 }
1358
1359 pub fn heal_mesh_if_needed(&self, topic: &TopicId) -> Vec<PeerId> {
1365 let d_low = self.config.mesh_n_low;
1366 let d_high = self.config.mesh_n_high;
1367
1368 let current_mesh: Vec<PeerId> = self
1369 .subscriptions
1370 .get(topic)
1371 .map(|s| s.mesh_peers.iter().cloned().collect())
1372 .unwrap_or_default();
1373
1374 if current_mesh.len() >= d_low {
1375 return Vec::new();
1376 }
1377
1378 let slots_needed = d_low.saturating_sub(current_mesh.len());
1379 let mesh_set: std::collections::HashSet<PeerId> = current_mesh.into_iter().collect();
1380
1381 let mut candidates: Vec<(PeerId, f64)> = self
1384 .peer_scores
1385 .iter()
1386 .filter(|entry| !mesh_set.contains(entry.key()))
1387 .map(|entry| (*entry.key(), entry.value().total_score))
1388 .collect();
1389
1390 candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1391
1392 let to_graft: Vec<PeerId> = candidates
1393 .into_iter()
1394 .take(slots_needed.min(d_high - mesh_set.len()))
1395 .map(|(peer, _)| peer)
1396 .collect();
1397
1398 for peer in &to_graft {
1399 let _ = self.add_peer_to_mesh(topic, *peer);
1401 }
1402
1403 to_graft
1404 }
1405
1406 pub fn prune_mesh_if_needed(&self, topic: &TopicId) -> Vec<PeerId> {
1411 let d_high = self.config.mesh_n_high;
1412
1413 let current_mesh: Vec<PeerId> = self
1414 .subscriptions
1415 .get(topic)
1416 .map(|s| s.mesh_peers.iter().cloned().collect())
1417 .unwrap_or_default();
1418
1419 if current_mesh.len() <= d_high {
1420 return Vec::new();
1421 }
1422
1423 let excess = current_mesh.len() - d_high;
1424
1425 let mut scored: Vec<(PeerId, f64)> = current_mesh
1427 .into_iter()
1428 .map(|peer| {
1429 let score = self
1430 .peer_scores
1431 .get(&peer)
1432 .map(|s| s.total_score)
1433 .unwrap_or(0.0);
1434 (peer, score)
1435 })
1436 .collect();
1437
1438 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1440
1441 let to_prune: Vec<PeerId> = scored.into_iter().take(excess).map(|(p, _)| p).collect();
1442
1443 for peer in &to_prune {
1444 let _ = self.remove_peer_from_mesh(topic, peer);
1445 }
1446
1447 to_prune
1448 }
1449}
1450
1451#[cfg(test)]
1456mod mesh_health_tests {
1457 use super::*;
1458 use libp2p::identity::Keypair;
1459
1460 fn test_peer_str(index: u8) -> String {
1461 let mut seed = [0u8; 32];
1462 seed[0] = index;
1463 let kp = Keypair::ed25519_from_bytes(seed).expect("valid seed");
1464 kp.public().to_peer_id().to_string()
1465 }
1466
1467 #[test]
1468 fn test_mesh_health_underpeered() {
1469 let monitor = MeshHealthMonitor::new(6, 12);
1470 let status = monitor.health_status(3);
1471 assert_eq!(
1472 status,
1473 MeshHealthStatus::Underpeered {
1474 current: 3,
1475 minimum: 6
1476 }
1477 );
1478 assert!(monitor.is_mesh_underpeered(3));
1479 }
1480
1481 #[test]
1482 fn test_mesh_health_healthy() {
1483 let monitor = MeshHealthMonitor::new(6, 12);
1484 let status = monitor.health_status(8);
1485 assert_eq!(status, MeshHealthStatus::Healthy);
1486 assert!(!monitor.is_mesh_underpeered(8));
1487 assert!(!monitor.is_mesh_overloaded(8));
1488 }
1489
1490 #[test]
1491 fn test_mesh_health_overloaded() {
1492 let monitor = MeshHealthMonitor::new(6, 12);
1493 let status = monitor.health_status(15);
1494 assert_eq!(
1495 status,
1496 MeshHealthStatus::Overloaded {
1497 current: 15,
1498 maximum: 12,
1499 }
1500 );
1501 assert!(monitor.is_mesh_overloaded(15));
1502 }
1503
1504 #[test]
1505 fn test_mesh_suggest_grafts() {
1506 let monitor = MeshHealthMonitor::new(6, 12);
1507 let a = test_peer_str(1);
1508 let b = test_peer_str(2);
1509 let c = test_peer_str(3);
1510 let d = test_peer_str(4);
1511
1512 let gossip_peers = vec![a.clone(), b.clone(), c.clone(), d.clone()];
1513 let mesh_peers = vec![a.clone()];
1514
1515 let suggestions = monitor.suggest_grafts(&gossip_peers, &mesh_peers);
1516
1517 assert_eq!(suggestions.len(), 3);
1519 assert!(!suggestions.contains(&a));
1520 assert!(suggestions.contains(&b));
1521 assert!(suggestions.contains(&c));
1522 assert!(suggestions.contains(&d));
1523 }
1524
1525 #[test]
1526 fn test_heal_backoff() {
1527 let mut monitor = MeshHealthMonitor::new(6, 12).with_heal_interval(60);
1528
1529 assert!(monitor.should_attempt_heal());
1531
1532 monitor.record_heal_attempt();
1534 assert!(
1535 !monitor.should_attempt_heal(),
1536 "should not heal immediately after attempt"
1537 );
1538
1539 assert_eq!(monitor.heal_attempts(), 1);
1540 }
1541
1542 #[test]
1543 fn test_gossipsub_manager_mesh_health_method() {
1544 let mgr = GossipSubManager::new(GossipSubConfig::default());
1545 let topic = TopicId::content_announce();
1546 mgr.subscribe(topic.clone()).expect("subscribe");
1547
1548 let status = mgr.mesh_health(&topic);
1550 assert!(
1551 matches!(status, MeshHealthStatus::Underpeered { .. }),
1552 "empty mesh should be underpeered"
1553 );
1554 }
1555
1556 #[test]
1557 fn test_gossipsub_manager_heal_mesh_if_needed() {
1558 let mgr = GossipSubManager::new(GossipSubConfig::default());
1559 let topic = TopicId::content_announce();
1560 mgr.subscribe(topic.clone()).expect("subscribe");
1561
1562 let grafted = mgr.heal_mesh_if_needed(&topic);
1564 assert!(grafted.is_empty(), "no scored peers to graft from");
1565 }
1566
1567 #[test]
1568 fn test_gossipsub_manager_prune_mesh_if_needed() {
1569 let config = GossipSubConfig {
1570 mesh_n_high: 2,
1571 ..GossipSubConfig::default()
1572 };
1573 let mgr = GossipSubManager::new(config);
1574 let topic = TopicId::content_announce();
1575 mgr.subscribe(topic.clone()).expect("subscribe");
1576
1577 for i in 0u8..4 {
1579 let mut seed = [0u8; 32];
1580 seed[0] = i + 10;
1581 let kp = Keypair::ed25519_from_bytes(seed).expect("seed");
1582 let peer = kp.public().to_peer_id();
1583 mgr.add_peer_to_mesh(&topic, peer).expect("add to mesh");
1584 }
1585
1586 let before = mgr.get_mesh_peers(&topic).expect("get peers").len();
1587 assert_eq!(before, 4);
1588
1589 let pruned = mgr.prune_mesh_if_needed(&topic);
1590 assert_eq!(pruned.len(), 2, "should prune 2 peers to reach d_high=2");
1591
1592 let after = mgr.get_mesh_peers(&topic).expect("get peers").len();
1593 assert_eq!(after, 2);
1594 }
1595}