1use libp2p::gossipsub::{IdentTopic, TopicHash};
15use std::collections::{HashMap, VecDeque};
16use std::time::Instant;
17
18pub struct GossipTopics {
20 topics: HashMap<String, IdentTopic>,
21}
22
23impl GossipTopics {
24 pub fn new() -> Self {
26 let mut topics = HashMap::new();
27
28 topics.insert("blocks".to_string(), IdentTopic::new("tenzro/blocks"));
30 topics.insert("transactions".to_string(), IdentTopic::new("tenzro/transactions"));
31 topics.insert("consensus".to_string(), IdentTopic::new("tenzro/consensus"));
32
33 topics.insert("attestations".to_string(), IdentTopic::new("tenzro/attestations"));
35 topics.insert("models".to_string(), IdentTopic::new("tenzro/models"));
36 topics.insert("inference".to_string(), IdentTopic::new("tenzro/inference"));
37
38 topics.insert("status".to_string(), IdentTopic::new("tenzro/status"));
40
41 topics.insert("agents".to_string(), IdentTopic::new("tenzro/agents"));
43 topics.insert("providers".to_string(), IdentTopic::new("tenzro/providers"));
44
45 topics.insert("cortex".to_string(), IdentTopic::new("tenzro/cortex"));
47
48 topics.insert("training".to_string(), IdentTopic::new("tenzro/training"));
50 topics.insert("training_syncer".to_string(), IdentTopic::new("tenzro/training/syncer"));
51
52 topics.insert("seed_agents".to_string(), IdentTopic::new("tenzro/seed-agents"));
54
55 Self { topics }
56 }
57
58 pub fn get(&self, name: &str) -> Option<&IdentTopic> {
60 self.topics.get(name)
61 }
62
63 pub fn all_hashes(&self) -> Vec<TopicHash> {
65 self.topics.values().map(|t| t.hash()).collect()
66 }
67
68 pub fn all(&self) -> Vec<&IdentTopic> {
70 self.topics.values().collect()
71 }
72
73 pub fn add_custom(&mut self, name: String, topic: IdentTopic) {
75 self.topics.insert(name, topic);
76 }
77
78 pub fn blocks(&self) -> &IdentTopic {
80 self.topics.get("blocks").expect("blocks topic must exist")
81 }
82
83 pub fn transactions(&self) -> &IdentTopic {
85 self.topics.get("transactions").expect("transactions topic must exist")
86 }
87
88 pub fn consensus(&self) -> &IdentTopic {
90 self.topics.get("consensus").expect("consensus topic must exist")
91 }
92
93 pub fn attestations(&self) -> &IdentTopic {
95 self.topics.get("attestations").expect("attestations topic must exist")
96 }
97
98 pub fn models(&self) -> &IdentTopic {
100 self.topics.get("models").expect("models topic must exist")
101 }
102
103 pub fn inference(&self) -> &IdentTopic {
105 self.topics.get("inference").expect("inference topic must exist")
106 }
107
108 pub fn status(&self) -> &IdentTopic {
110 self.topics.get("status").expect("status topic must exist")
111 }
112
113 pub fn agents(&self) -> &IdentTopic {
115 self.topics.get("agents").expect("agents topic must exist")
116 }
117
118 pub fn providers(&self) -> &IdentTopic {
120 self.topics.get("providers").expect("providers topic must exist")
121 }
122
123 pub fn cortex(&self) -> &IdentTopic {
125 self.topics.get("cortex").expect("cortex topic must exist")
126 }
127
128 pub fn seed_agents(&self) -> &IdentTopic {
130 self.topics.get("seed_agents").expect("seed_agents topic must exist")
131 }
132}
133
134impl Default for GossipTopics {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140pub struct TopicSubscriptions {
142 subscribed: HashMap<TopicHash, bool>,
143}
144
145impl TopicSubscriptions {
146 pub fn new() -> Self {
148 Self {
149 subscribed: HashMap::new(),
150 }
151 }
152
153 pub fn subscribe(&mut self, topic: &TopicHash) {
155 self.subscribed.insert(topic.clone(), true);
156 }
157
158 pub fn unsubscribe(&mut self, topic: &TopicHash) {
160 self.subscribed.remove(topic);
161 }
162
163 pub fn is_subscribed(&self, topic: &TopicHash) -> bool {
165 self.subscribed.get(topic).copied().unwrap_or(false)
166 }
167
168 pub fn subscribed_topics(&self) -> Vec<&TopicHash> {
170 self.subscribed.keys().collect()
171 }
172
173 pub fn count(&self) -> usize {
175 self.subscribed.len()
176 }
177}
178
179impl Default for TopicSubscriptions {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185pub struct MessageDeduplicator {
190 seen: VecDeque<([u8; 32], Instant)>,
192 seen_set: HashMap<[u8; 32], ()>,
194 max_entries: usize,
196 expiry_secs: u64,
198}
199
200impl MessageDeduplicator {
201 pub fn new(max_entries: usize, expiry_secs: u64) -> Self {
203 Self {
204 seen: VecDeque::with_capacity(max_entries),
205 seen_set: HashMap::with_capacity(max_entries),
206 max_entries,
207 expiry_secs,
208 }
209 }
210
211 pub fn is_duplicate(&mut self, data: &[u8]) -> bool {
214 use sha2::{Digest, Sha256};
215 let hash: [u8; 32] = Sha256::digest(data).into();
216
217 let now = Instant::now();
219 while let Some(&(_, ts)) = self.seen.front() {
220 if now.duration_since(ts).as_secs() > self.expiry_secs {
221 if let Some((old_hash, _)) = self.seen.pop_front() {
222 self.seen_set.remove(&old_hash);
223 }
224 } else {
225 break;
226 }
227 }
228
229 if self.seen.len() >= self.max_entries
231 && let Some((old_hash, _)) = self.seen.pop_front()
232 {
233 self.seen_set.remove(&old_hash);
234 }
235
236 if self.seen_set.contains_key(&hash) {
237 return true;
238 }
239
240 self.seen_set.insert(hash, ());
241 self.seen.push_back((hash, now));
242 false
243 }
244
245 pub fn len(&self) -> usize {
247 self.seen.len()
248 }
249
250 pub fn is_empty(&self) -> bool {
252 self.seen.is_empty()
253 }
254}
255
256impl Default for MessageDeduplicator {
257 fn default() -> Self {
258 Self::new(100_000, 300)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum MessageValidation {
266 Accept,
268 Reject,
270 Ignore,
272}
273
274pub fn validate_gossip_message(_topic: &TopicHash, data: &[u8]) -> MessageValidation {
276 if data.is_empty() {
278 return MessageValidation::Reject;
279 }
280
281 if data.len() > 1_048_576 {
284 return MessageValidation::Reject;
285 }
286
287 match crate::message::NetworkMessage::from_bytes(data) {
289 Ok(msg) => {
290 if let Err(e) = crate::message::validate_message(&msg) {
292 tracing::warn!("Message validation failed: {}", e);
293 return MessageValidation::Reject;
294 }
295 MessageValidation::Accept
296 }
297 Err(e) => {
298 tracing::warn!("Failed to parse network message: {}", e);
299 MessageValidation::Reject
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_gossip_topics() {
310 let topics = GossipTopics::new();
311 assert!(topics.get("blocks").is_some());
312 assert!(topics.get("transactions").is_some());
313 assert!(topics.get("consensus").is_some());
314 assert!(topics.get("nonexistent").is_none());
315 }
316
317 #[test]
318 fn test_topic_subscriptions() {
319 let mut subs = TopicSubscriptions::new();
320 let topics = GossipTopics::new();
321 let blocks_hash = topics.blocks().hash();
322
323 assert!(!subs.is_subscribed(&blocks_hash));
324
325 subs.subscribe(&blocks_hash);
326 assert!(subs.is_subscribed(&blocks_hash));
327 assert_eq!(subs.count(), 1);
328
329 subs.unsubscribe(&blocks_hash);
330 assert!(!subs.is_subscribed(&blocks_hash));
331 assert_eq!(subs.count(), 0);
332 }
333
334 #[test]
335 fn test_message_deduplication() {
336 let mut dedup = MessageDeduplicator::new(100, 60);
337
338 let msg1 = b"message one";
339 let msg2 = b"message two";
340
341 assert!(!dedup.is_duplicate(msg1));
343 assert!(dedup.is_duplicate(msg1));
345 assert!(!dedup.is_duplicate(msg2));
347
348 assert_eq!(dedup.len(), 2);
349 }
350
351 #[test]
352 fn test_dedup_capacity_eviction() {
353 let mut dedup = MessageDeduplicator::new(3, 300);
354
355 assert!(!dedup.is_duplicate(b"a"));
356 assert!(!dedup.is_duplicate(b"b"));
357 assert!(!dedup.is_duplicate(b"c"));
358 assert!(!dedup.is_duplicate(b"d"));
360 assert_eq!(dedup.len(), 3);
361 assert!(!dedup.is_duplicate(b"a"));
363 }
364
365 #[test]
366 fn test_message_validation() {
367 assert_eq!(
369 validate_gossip_message(&TopicHash::from_raw("test"), &[]),
370 MessageValidation::Reject
371 );
372
373 let msg = crate::message::NetworkMessage::new(crate::message::MessagePayload::Ping);
375 let bytes = msg.to_bytes().unwrap();
376 let topics = GossipTopics::new();
377 let result = validate_gossip_message(&topics.status().hash(), &bytes);
378 assert_eq!(result, MessageValidation::Accept);
379 }
380}