Skip to main content

clawft_kernel/
mesh_bootstrap.rs

1//! Static seed peer bootstrap discovery (K6.2).
2//!
3//! The simplest discovery mechanism: connect to a preconfigured list
4//! of seed peer addresses. Always available, no extra dependencies.
5//!
6//! # Status (WEFT-151 audit)
7//!
8//! **Orphan — library only.** No production callers outside this module
9//! and re-exports. `boot.rs` connects `mesh_config.seed_peers` by
10//! iterating addresses and calling `MeshTransport::connect` directly;
11//! it never constructs [`BootstrapDiscovery`] or
12//! [`PeerExchangeDiscovery`]. [`crate::mesh_discovery::DiscoveryCoordinator`]
13//! is itself only exercised in its unit tests (no daemon registration).
14//!
15//! **Disposition:** keep as `DiscoveryBackend` implementations.
16//! **Wiring schedule (0.8.x):** in boot mesh init, build a
17//! `DiscoveryCoordinator`, `add_backend(Box::new(BootstrapDiscovery::new(seed_peers)))`,
18//! optionally PEX after handshake, and feed discovered peers into
19//! the existing seed-connect loop / `MeshRuntime`.
20
21use crate::mesh_discovery::{DiscoveredPeer, DiscoveryBackend, DiscoveryError, DiscoverySource};
22use async_trait::async_trait;
23
24/// Bootstrap discovery from static seed peers.
25pub struct BootstrapDiscovery {
26    /// Seed peer addresses (e.g., "quic://192.168.1.100:9489").
27    seed_addresses: Vec<String>,
28    /// Peers discovered from seed connections.
29    discovered: Vec<DiscoveredPeer>,
30    /// Whether initial bootstrap has been performed.
31    bootstrapped: bool,
32}
33
34impl BootstrapDiscovery {
35    /// Create a new bootstrap discovery with seed peer addresses.
36    pub fn new(seed_addresses: Vec<String>) -> Self {
37        Self {
38            seed_addresses,
39            discovered: Vec::new(),
40            bootstrapped: false,
41        }
42    }
43
44    /// Get the configured seed addresses.
45    pub fn seed_addresses(&self) -> &[String] {
46        &self.seed_addresses
47    }
48
49    /// Add a seed address dynamically.
50    pub fn add_seed(&mut self, address: String) {
51        if !self.seed_addresses.contains(&address) {
52            self.seed_addresses.push(address);
53        }
54    }
55}
56
57#[async_trait]
58impl DiscoveryBackend for BootstrapDiscovery {
59    fn name(&self) -> &str {
60        "bootstrap"
61    }
62
63    async fn start(&mut self) -> Result<(), DiscoveryError> {
64        // On start, produce DiscoveredPeer for each seed address.
65        // Actual connection happens in the mesh listener, not here.
66        // Discovery just reports "these peers exist, try connecting."
67        if !self.bootstrapped {
68            for (i, addr) in self.seed_addresses.iter().enumerate() {
69                self.discovered.push(DiscoveredPeer {
70                    node_id: format!("seed-{i}"),
71                    address: addr.clone(),
72                    platform: "unknown".into(),
73                    source: DiscoverySource::SeedPeer,
74                });
75            }
76            self.bootstrapped = true;
77        }
78        Ok(())
79    }
80
81    async fn poll(&mut self) -> Vec<DiscoveredPeer> {
82        // Return any undrained discovered peers, then clear.
83        std::mem::take(&mut self.discovered)
84    }
85
86    async fn stop(&mut self) -> Result<(), DiscoveryError> {
87        self.discovered.clear();
88        Ok(())
89    }
90}
91
92/// Peer exchange discovery: learns about peers from other peers' lists.
93pub struct PeerExchangeDiscovery {
94    /// Peers learned from peer list exchange during handshake.
95    pending: Vec<DiscoveredPeer>,
96}
97
98impl PeerExchangeDiscovery {
99    pub fn new() -> Self {
100        Self {
101            pending: Vec::new(),
102        }
103    }
104
105    /// Add peers learned from a remote node's peer list.
106    pub fn add_peers_from_exchange(&mut self, peers: Vec<DiscoveredPeer>) {
107        for mut peer in peers {
108            peer.source = DiscoverySource::PeerExchange;
109            self.pending.push(peer);
110        }
111    }
112}
113
114impl Default for PeerExchangeDiscovery {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[async_trait]
121impl DiscoveryBackend for PeerExchangeDiscovery {
122    fn name(&self) -> &str {
123        "peer-exchange"
124    }
125
126    async fn start(&mut self) -> Result<(), DiscoveryError> {
127        Ok(())
128    }
129
130    async fn poll(&mut self) -> Vec<DiscoveredPeer> {
131        std::mem::take(&mut self.pending)
132    }
133
134    async fn stop(&mut self) -> Result<(), DiscoveryError> {
135        self.pending.clear();
136        Ok(())
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[tokio::test]
145    async fn bootstrap_start_produces_peers() {
146        let seeds = vec![
147            "quic://10.0.0.1:9489".to_string(),
148            "quic://10.0.0.2:9489".to_string(),
149        ];
150        let mut disc = BootstrapDiscovery::new(seeds);
151        disc.start().await.unwrap();
152
153        let peers = disc.poll().await;
154        assert_eq!(peers.len(), 2);
155        assert_eq!(peers[0].node_id, "seed-0");
156        assert_eq!(peers[0].address, "quic://10.0.0.1:9489");
157        assert_eq!(peers[0].source, DiscoverySource::SeedPeer);
158        assert_eq!(peers[1].node_id, "seed-1");
159    }
160
161    #[tokio::test]
162    async fn bootstrap_poll_drains() {
163        let mut disc = BootstrapDiscovery::new(vec!["addr".into()]);
164        disc.start().await.unwrap();
165
166        let first = disc.poll().await;
167        assert_eq!(first.len(), 1);
168
169        let second = disc.poll().await;
170        assert!(second.is_empty(), "second poll should be empty");
171    }
172
173    #[tokio::test]
174    async fn bootstrap_double_start_no_duplicate() {
175        let mut disc = BootstrapDiscovery::new(vec!["a".into()]);
176        disc.start().await.unwrap();
177        disc.start().await.unwrap(); // second start is a no-op
178
179        let peers = disc.poll().await;
180        assert_eq!(peers.len(), 1, "should not duplicate on second start");
181    }
182
183    #[tokio::test]
184    async fn bootstrap_add_seed() {
185        let mut disc = BootstrapDiscovery::new(vec!["a".into()]);
186        disc.add_seed("b".into());
187        disc.add_seed("a".into()); // duplicate, should be ignored
188        assert_eq!(disc.seed_addresses().len(), 2);
189    }
190
191    #[tokio::test]
192    async fn bootstrap_stop_clears() {
193        let mut disc = BootstrapDiscovery::new(vec!["a".into()]);
194        disc.start().await.unwrap();
195        disc.stop().await.unwrap();
196
197        let peers = disc.poll().await;
198        assert!(peers.is_empty());
199    }
200
201    #[tokio::test]
202    async fn peer_exchange_add_and_poll() {
203        let mut pex = PeerExchangeDiscovery::new();
204        assert_eq!(pex.name(), "peer-exchange");
205
206        pex.start().await.unwrap();
207
208        let incoming = vec![DiscoveredPeer {
209            node_id: "remote-1".into(),
210            address: "quic://192.168.1.50:9489".into(),
211            platform: "darwin".into(),
212            source: DiscoverySource::Manual, // will be overwritten
213        }];
214        pex.add_peers_from_exchange(incoming);
215
216        let peers = pex.poll().await;
217        assert_eq!(peers.len(), 1);
218        assert_eq!(peers[0].source, DiscoverySource::PeerExchange);
219        assert_eq!(peers[0].node_id, "remote-1");
220
221        let second = pex.poll().await;
222        assert!(second.is_empty());
223    }
224
225    #[tokio::test]
226    async fn peer_exchange_stop() {
227        let mut pex = PeerExchangeDiscovery::new();
228        pex.add_peers_from_exchange(vec![DiscoveredPeer {
229            node_id: "n".into(),
230            address: "a".into(),
231            platform: "p".into(),
232            source: DiscoverySource::Manual,
233        }]);
234        pex.stop().await.unwrap();
235        let peers = pex.poll().await;
236        assert!(peers.is_empty());
237    }
238}