1use super::gossip_client::GossipClient;
4use super::mdns_discovery::MdnsDiscovery;
5use async_trait::async_trait;
6use pot_o_core::TribeResult;
7use pot_o_mining::{Challenge, ProofPayload};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12pub type NodeId = String;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PeerInfo {
20 pub node_id: NodeId,
21 pub address: String,
22 pub port: u16,
23 pub last_seen: chrono::DateTime<chrono::Utc>,
24 pub version: String,
25}
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct NetworkState {
29 pub peers: Vec<PeerInfo>,
30 pub total_nodes: usize,
31 pub synced: bool,
32}
33
34#[allow(clippy::double_must_use)]
40#[async_trait]
41pub trait PeerNetwork: Send + Sync {
42 fn node_id(&self) -> &NodeId;
43 async fn discover_peers(&self) -> TribeResult<Vec<PeerInfo>>;
44 async fn broadcast_challenge(&self, challenge: &Challenge) -> TribeResult<()>;
45 async fn relay_proof(&self, proof: &ProofPayload) -> TribeResult<()>;
46 async fn sync_state(&self) -> TribeResult<NetworkState>;
47
48 async fn broadcast_transaction(&self, _tx: &serde_json::Value) -> TribeResult<()> {
50 Ok(())
51 }
52
53 async fn pull_lattice(&self, peer_url: &str) -> TribeResult<Option<serde_json::Value>> {
56 #[allow(unused)]
57 let _ = peer_url;
58 Ok(None)
59 }
60
61 async fn push_lattice(&self, _snapshot: &serde_json::Value) -> TribeResult<usize> {
64 Ok(0)
65 }
66
67 async fn register_with_bootstrap(&self, _cfg: &RegistrationConfig) -> TribeResult<()> {
70 Ok(())
71 }
72
73 async fn broadcast_block(&self, _proof: &serde_json::Value) -> TribeResult<usize> {
76 Ok(0)
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct RegistrationConfig {
83 pub node_id: String,
84 pub address: String,
85 pub port: u16,
86 pub version: String,
87 pub bootstrap_urls: Vec<String>,
88 pub timeout_secs: u64,
89}
90
91pub struct LocalOnlyNetwork {
96 node_id: NodeId,
97}
98
99impl LocalOnlyNetwork {
100 pub fn new() -> Self {
101 Self {
102 node_id: uuid::Uuid::new_v4().to_string(),
103 }
104 }
105}
106
107impl Default for LocalOnlyNetwork {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113#[async_trait]
114impl PeerNetwork for LocalOnlyNetwork {
115 fn node_id(&self) -> &NodeId {
116 &self.node_id
117 }
118
119 async fn discover_peers(&self) -> TribeResult<Vec<PeerInfo>> {
120 Ok(vec![]) }
122
123 async fn broadcast_challenge(&self, _challenge: &Challenge) -> TribeResult<()> {
124 Ok(()) }
126
127 async fn relay_proof(&self, _proof: &ProofPayload) -> TribeResult<()> {
128 Ok(()) }
130
131 async fn sync_state(&self) -> TribeResult<NetworkState> {
132 Ok(NetworkState {
133 peers: vec![],
134 total_nodes: 1,
135 synced: true,
136 })
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VpnMeshConfig {
146 pub wireguard_interface: String,
147 pub peer_addresses: Vec<String>,
148 pub mdns_enabled: bool,
149 pub gossip_port: u16,
150}
151
152pub struct VpnMeshNetwork {
153 pub node_id: NodeId,
154 pub config: VpnMeshConfig,
155 gossip_client: GossipClient,
156 mdns_discovery: Option<Arc<MdnsDiscovery>>,
157 bootstrap_urls: Vec<String>,
158 peer_list: Arc<RwLock<Vec<PeerInfo>>>,
159 peer_timeout_secs: u64,
160}
161
162impl VpnMeshNetwork {
163 pub fn new(
176 node_id: NodeId,
177 config: VpnMeshConfig,
178 bootstrap_urls: Vec<String>,
179 mdns_enabled: bool,
180 _mdns_service_name: &str,
181 peer_timeout_secs: u64,
182 ) -> Result<Self, Box<dyn std::error::Error>> {
183 let gossip_client = GossipClient::new(bootstrap_urls.clone(), peer_timeout_secs);
185
186 let mdns_discovery = if mdns_enabled && config.mdns_enabled {
188 Some(Arc::new(MdnsDiscovery::new(&node_id, config.gossip_port)?))
189 } else {
190 None
191 };
192
193 Ok(Self {
194 node_id,
195 config,
196 gossip_client,
197 mdns_discovery,
198 bootstrap_urls,
199 peer_list: Arc::new(RwLock::new(Vec::new())),
200 peer_timeout_secs,
201 })
202 }
203
204 async fn fetch_bootstrap_peers(&self) -> Result<Vec<PeerInfo>, Box<dyn std::error::Error>> {
206 let mut peers = Vec::new();
207
208 for bootstrap_url in &self.bootstrap_urls {
209 let url = format!("{}/peers", bootstrap_url);
211 match reqwest::Client::new()
212 .get(&url)
213 .timeout(std::time::Duration::from_secs(self.peer_timeout_secs))
214 .send()
215 .await
216 {
217 Ok(response) if response.status().is_success() => {
218 if let Ok(peer_infos) = response.json::<Vec<PeerInfo>>().await {
219 peers.extend(peer_infos);
220 }
221 }
222 _ => {
223 }
225 }
226 }
227
228 peers.sort_by(|a, b| a.node_id.cmp(&b.node_id));
230 peers.dedup_by(|a, b| a.node_id == b.node_id);
231
232 Ok(peers)
233 }
234}
235
236#[async_trait]
237impl PeerNetwork for VpnMeshNetwork {
238 fn node_id(&self) -> &NodeId {
239 &self.node_id
240 }
241
242 async fn discover_peers(&self) -> TribeResult<Vec<PeerInfo>> {
243 let mut all_peers = Vec::new();
244
245 if let Some(mdns) = &self.mdns_discovery {
249 let mdns_clone = mdns.clone();
250 let discovered = tokio::task::spawn_blocking(move || {
251 mdns_clone.discover_peers(5).unwrap_or_default()
252 })
253 .await
254 .unwrap_or_default();
255 for pd in discovered {
256 all_peers.push(PeerInfo {
257 node_id: pd.node_id,
258 address: pd.ip.to_string(),
259 port: pd.port,
260 last_seen: chrono::Utc::now(),
261 version: "1.0".to_string(),
262 });
263 }
264 }
265
266 match self.fetch_bootstrap_peers().await {
268 Ok(bootstrap_peers) => {
269 all_peers.extend(bootstrap_peers);
270 }
271 Err(_) => {
272 }
274 }
275
276 all_peers.sort_by(|a, b| a.node_id.cmp(&b.node_id));
278 all_peers.dedup_by(|a, b| a.node_id == b.node_id);
279
280 all_peers.retain(|peer| peer.node_id != self.node_id);
282
283 {
285 let mut peer_list = self.peer_list.write().await;
286 *peer_list = all_peers.clone();
287 }
288
289 Ok(all_peers)
290 }
291
292 async fn broadcast_challenge(&self, challenge: &Challenge) -> TribeResult<()> {
293 let challenge_json = serde_json::to_value(challenge).map_err(|e| {
295 pot_o_core::TribeError::SerializationError(format!(
296 "Failed to serialize challenge: {}",
297 e
298 ))
299 })?;
300
301 let _success_count = self
303 .gossip_client
304 .broadcast_challenge(&self.node_id, &challenge_json)
305 .await
306 .map_err(|e| {
307 pot_o_core::TribeError::NetworkError(format!(
308 "Failed to broadcast challenge: {}",
309 e
310 ))
311 })?;
312
313 Ok(())
314 }
315
316 async fn relay_proof(&self, proof: &ProofPayload) -> TribeResult<()> {
317 let proof_json = serde_json::to_value(proof).map_err(|e| {
319 pot_o_core::TribeError::SerializationError(format!("Failed to serialize proof: {}", e))
320 })?;
321
322 let payload = serde_json::json!({
324 "node_id": self.node_id,
325 "proof": proof_json,
326 });
327
328 let peers = self.peer_list.read().await;
330 for peer in peers.iter() {
331 let peer_url = format!("http://{}:{}", peer.address, peer.port);
332 let _result = reqwest::Client::new()
333 .post(format!("{}/proof", peer_url))
334 .timeout(std::time::Duration::from_secs(self.peer_timeout_secs))
335 .json(&payload)
336 .send()
337 .await;
338 }
340
341 Ok(())
342 }
343
344 async fn sync_state(&self) -> TribeResult<NetworkState> {
345 let peer_list = self.peer_list.read().await;
346 let total_nodes = peer_list.len() + 1; Ok(NetworkState {
349 peers: peer_list.clone(),
350 total_nodes,
351 synced: !peer_list.is_empty(),
352 })
353 }
354
355 async fn pull_lattice(&self, peer_url: &str) -> TribeResult<Option<serde_json::Value>> {
356 let url = format!("{}/hexchain/lattice", peer_url.trim_end_matches('/'));
357 match reqwest::Client::new()
358 .get(&url)
359 .timeout(std::time::Duration::from_secs(self.peer_timeout_secs))
360 .send()
361 .await
362 {
363 Ok(resp) if resp.status().is_success() => {
364 match resp.json::<serde_json::Value>().await {
365 Ok(json) => Ok(Some(json)),
366 Err(_) => Ok(None),
367 }
368 }
369 _ => Ok(None),
370 }
371 }
372
373 async fn push_lattice(&self, snapshot: &serde_json::Value) -> TribeResult<usize> {
374 let peers = self.peer_list.read().await;
375 let mut success = 0usize;
376 for peer in peers.iter() {
377 let url = format!(
378 "http://{}:{}/hexchain/lattice/sync",
379 peer.address, peer.port
380 );
381 match reqwest::Client::new()
382 .post(&url)
383 .timeout(std::time::Duration::from_secs(self.peer_timeout_secs))
384 .json(snapshot)
385 .send()
386 .await
387 {
388 Ok(resp) if resp.status().is_success() => success += 1,
389 _ => {}
390 }
391 }
392 Ok(success)
393 }
394
395 async fn broadcast_transaction(&self, tx: &serde_json::Value) -> TribeResult<()> {
396 let peers = self.peer_list.read().await;
397 for peer in peers.iter() {
398 let url = format!(
399 "http://{}:{}/internal/tx/broadcast",
400 peer.address, peer.port
401 );
402 let _ = reqwest::Client::new()
403 .post(&url)
404 .timeout(std::time::Duration::from_secs(self.peer_timeout_secs))
405 .json(tx)
406 .send()
407 .await;
408 }
409 Ok(())
410 }
411
412 async fn register_with_bootstrap(&self, cfg: &RegistrationConfig) -> TribeResult<()> {
413 let body = serde_json::json!({
414 "node_id": cfg.node_id,
415 "address": cfg.address,
416 "port": cfg.port,
417 "version": cfg.version,
418 });
419 for url in &cfg.bootstrap_urls {
420 let register_url = format!("{}/register", url.trim_end_matches('/'));
421 let _ = reqwest::Client::new()
422 .post(®ister_url)
423 .timeout(std::time::Duration::from_secs(cfg.timeout_secs))
424 .json(&body)
425 .send()
426 .await;
427 }
428 Ok(())
429 }
430
431 async fn broadcast_block(&self, proof: &serde_json::Value) -> TribeResult<usize> {
432 self.gossip_client
433 .broadcast_block(proof)
434 .await
435 .map_err(|e| {
436 pot_o_core::TribeError::NetworkError(format!("Failed to broadcast block: {}", e))
437 })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 fn create_test_peer(node_id: &str, address: &str, port: u16) -> PeerInfo {
447 PeerInfo {
448 node_id: node_id.to_string(),
449 address: address.to_string(),
450 port,
451 last_seen: chrono::Utc::now(),
452 version: "1.0".to_string(),
453 }
454 }
455
456 #[test]
457 fn test_vpn_mesh_network_creation() {
458 let config = VpnMeshConfig {
459 wireguard_interface: "wg0".to_string(),
460 peer_addresses: vec![],
461 mdns_enabled: false,
462 gossip_port: 8765,
463 };
464
465 let result = VpnMeshNetwork::new(
466 "test-node-1".to_string(),
467 config,
468 vec![],
469 false,
470 "pot-o-validator",
471 30,
472 );
473
474 assert!(result.is_ok());
475 let network = result.unwrap();
476 assert_eq!(network.node_id(), "test-node-1");
477 }
478
479 #[test]
480 fn test_vpn_mesh_network_with_bootstrap_urls() {
481 let config = VpnMeshConfig {
482 wireguard_interface: "wg0".to_string(),
483 peer_addresses: vec![],
484 mdns_enabled: false,
485 gossip_port: 8765,
486 };
487
488 let bootstrap_urls = vec![
489 "http://bootstrap1.local:8765".to_string(),
490 "http://bootstrap2.local:8765".to_string(),
491 ];
492
493 let result = VpnMeshNetwork::new(
494 "test-node-2".to_string(),
495 config,
496 bootstrap_urls.clone(),
497 false,
498 "pot-o-validator",
499 30,
500 );
501
502 assert!(result.is_ok());
503 let network = result.unwrap();
504 assert_eq!(network.bootstrap_urls, bootstrap_urls);
505 }
506
507 #[tokio::test]
508 async fn test_get_peers_returns_empty_initially() {
509 let config = VpnMeshConfig {
510 wireguard_interface: "wg0".to_string(),
511 peer_addresses: vec![],
512 mdns_enabled: false,
513 gossip_port: 8765,
514 };
515
516 let network = VpnMeshNetwork::new(
517 "test-node-3".to_string(),
518 config,
519 vec![],
520 false,
521 "pot-o-validator",
522 30,
523 )
524 .unwrap();
525
526 let peers = network.peer_list.read().await;
527 assert_eq!(peers.len(), 0);
528 }
529
530 #[tokio::test]
531 async fn test_node_id_returns_correct_value() {
532 let config = VpnMeshConfig {
533 wireguard_interface: "wg0".to_string(),
534 peer_addresses: vec![],
535 mdns_enabled: false,
536 gossip_port: 8765,
537 };
538
539 let node_id = "my-test-node".to_string();
540 let network = VpnMeshNetwork::new(
541 node_id.clone(),
542 config,
543 vec![],
544 false,
545 "pot-o-validator",
546 30,
547 )
548 .unwrap();
549
550 assert_eq!(network.node_id(), &node_id);
551 }
552
553 #[tokio::test]
554 async fn test_sync_state_with_empty_peers() {
555 let config = VpnMeshConfig {
556 wireguard_interface: "wg0".to_string(),
557 peer_addresses: vec![],
558 mdns_enabled: false,
559 gossip_port: 8765,
560 };
561
562 let network = VpnMeshNetwork::new(
563 "test-node-4".to_string(),
564 config,
565 vec![],
566 false,
567 "pot-o-validator",
568 30,
569 )
570 .unwrap();
571
572 let state = network.sync_state().await.unwrap();
573 assert_eq!(state.peers.len(), 0);
574 assert_eq!(state.total_nodes, 1);
575 assert!(!state.synced);
576 }
577
578 #[tokio::test]
579 async fn test_sync_state_with_peers() {
580 let config = VpnMeshConfig {
581 wireguard_interface: "wg0".to_string(),
582 peer_addresses: vec![],
583 mdns_enabled: false,
584 gossip_port: 8765,
585 };
586
587 let network = VpnMeshNetwork::new(
588 "test-node-5".to_string(),
589 config,
590 vec![],
591 false,
592 "pot-o-validator",
593 30,
594 )
595 .unwrap();
596
597 {
599 let mut peer_list = network.peer_list.write().await;
600 peer_list.push(create_test_peer("peer-1", "192.168.1.1", 8765));
601 peer_list.push(create_test_peer("peer-2", "192.168.1.2", 8765));
602 }
603
604 let state = network.sync_state().await.unwrap();
605 assert_eq!(state.peers.len(), 2);
606 assert_eq!(state.total_nodes, 3); assert!(state.synced);
608 }
609
610 #[tokio::test]
611 async fn test_discover_peers_filters_self() {
612 let config = VpnMeshConfig {
613 wireguard_interface: "wg0".to_string(),
614 peer_addresses: vec![],
615 mdns_enabled: false,
616 gossip_port: 8765,
617 };
618
619 let node_id = "test-node-6".to_string();
620 let network = VpnMeshNetwork::new(
621 node_id.clone(),
622 config,
623 vec![],
624 false,
625 "pot-o-validator",
626 30,
627 )
628 .unwrap();
629
630 {
632 let mut peer_list = network.peer_list.write().await;
633 peer_list.push(create_test_peer(&node_id, "192.168.1.0", 8765)); peer_list.push(create_test_peer("peer-1", "192.168.1.1", 8765)); }
636
637 let state = network.sync_state().await.unwrap();
639 assert_eq!(state.peers.len(), 2); assert_eq!(state.total_nodes, 3); }
643
644 #[tokio::test]
645 async fn test_broadcast_challenge_succeeds() {
646 let config = VpnMeshConfig {
647 wireguard_interface: "wg0".to_string(),
648 peer_addresses: vec![],
649 mdns_enabled: false,
650 gossip_port: 8765,
651 };
652
653 let network = VpnMeshNetwork::new(
654 "test-node-7".to_string(),
655 config,
656 vec![],
657 false,
658 "pot-o-validator",
659 30,
660 )
661 .unwrap();
662
663 let result = network.sync_state().await;
667 assert!(result.is_ok());
668 }
669
670 #[tokio::test]
671 async fn test_relay_proof_with_no_peers() {
672 let config = VpnMeshConfig {
673 wireguard_interface: "wg0".to_string(),
674 peer_addresses: vec![],
675 mdns_enabled: false,
676 gossip_port: 8765,
677 };
678
679 let network = VpnMeshNetwork::new(
680 "test-node-8".to_string(),
681 config,
682 vec![],
683 false,
684 "pot-o-validator",
685 30,
686 )
687 .unwrap();
688
689 let state = network.sync_state().await.unwrap();
691 assert_eq!(state.peers.len(), 0);
692 }
693}