Skip to main content

tenzro_network/
discovery.rs

1//! Peer discovery via Kademlia DHT for Tenzro Network
2
3use libp2p::{
4    kad::{store::MemoryStore, Behaviour as Kademlia, Config as KademliaConfig, Mode},
5    Multiaddr, PeerId,
6};
7use std::time::Duration;
8use tenzro_types::network::NetworkRole;
9
10/// Creates a Kademlia DHT behaviour for peer discovery
11pub fn create_kademlia(local_peer_id: PeerId) -> Kademlia<MemoryStore> {
12    let mut config = KademliaConfig::new(libp2p::StreamProtocol::new("/tenzro/kad"));
13
14    // A+++ 2026 hardening:
15    //   * S/Kademlia disjoint query paths — Sybil-resistance per Baumgart & Mies 2007.
16    //   * 30s query timeout — 2x faster failure detection than default 60s.
17    //   * k=10 replication — tuned for ≤200-peer meshes (vs 20 which is Ethereum-mainnet-scale overhead).
18    //   * OnConnected k-bucket inserts — avoid polluting routing table with unreachable peers.
19    //   * Long-lived records with 36h TTL; re-publish every 22h (matches IPFS public DHT).
20    config.set_query_timeout(Duration::from_secs(30));
21    config.set_replication_factor(std::num::NonZeroUsize::new(10).unwrap());
22    config.set_publication_interval(Some(Duration::from_secs(22 * 60 * 60)));
23    config.set_record_ttl(Some(Duration::from_secs(36 * 60 * 60)));
24    config.set_provider_record_ttl(Some(Duration::from_secs(24 * 60 * 60)));
25    config.disjoint_query_paths(true);
26
27    // Create the Kademlia behaviour with memory store
28    let store = MemoryStore::new(local_peer_id);
29    let mut kademlia = Kademlia::with_config(local_peer_id, store, config);
30
31    // Set server mode to enable incoming queries
32    kademlia.set_mode(Some(Mode::Server));
33
34    kademlia
35}
36
37/// Connects to bootstrap nodes on startup
38///
39/// This function extracts peer IDs from the multiaddresses and adds them to the
40/// Kademlia DHT routing table, then initiates the DHT bootstrap process for
41/// ongoing peer discovery.
42pub fn connect_to_bootstrap_nodes(
43    kademlia: &mut Kademlia<MemoryStore>,
44    config: &BootstrapConfig,
45) -> Vec<Multiaddr> {
46    let mut addrs_to_dial = Vec::new();
47
48    for addr in &config.boot_nodes {
49        // Extract peer ID from multiaddr if present
50        let peer_id = addr.iter().find_map(|proto| {
51            if let libp2p::multiaddr::Protocol::P2p(peer_id) = proto {
52                Some(peer_id)
53            } else {
54                None
55            }
56        });
57
58        if let Some(peer_id) = peer_id {
59            // Add address to Kademlia routing table
60            kademlia.add_address(&peer_id, addr.clone());
61            tracing::debug!("Added boot node {} to Kademlia", peer_id);
62        }
63
64        addrs_to_dial.push(addr.clone());
65    }
66
67    // Start the DHT bootstrap process
68    if !config.boot_nodes.is_empty() {
69        if let Err(e) = kademlia.bootstrap() {
70            tracing::warn!("Failed to start DHT bootstrap: {:?}", e);
71        } else {
72            tracing::info!("Started DHT bootstrap with {} boot nodes", config.boot_nodes.len());
73        }
74    }
75
76    addrs_to_dial
77}
78
79/// Bootstrap the DHT by connecting to known nodes
80///
81/// This is the lower-level function that directly accepts peer IDs and multiaddresses.
82/// For most use cases, prefer `connect_to_bootstrap_nodes` which uses `BootstrapConfig`.
83pub fn bootstrap_dht(kademlia: &mut Kademlia<MemoryStore>, boot_nodes: Vec<(PeerId, Multiaddr)>) {
84    for (peer_id, addr) in boot_nodes {
85        kademlia.add_address(&peer_id, addr);
86    }
87
88    // Start the bootstrap process
89    if let Err(e) = kademlia.bootstrap() {
90        tracing::warn!("Failed to start DHT bootstrap: {:?}", e);
91    } else {
92        tracing::info!("Started DHT bootstrap");
93    }
94}
95
96/// Provider record key for different provider types
97pub fn provider_key(provider_type: ProviderType) -> Vec<u8> {
98    match provider_type {
99        ProviderType::Inference => b"/tenzro/providers/inference".to_vec(),
100        ProviderType::Tee => b"/tenzro/providers/tee".to_vec(),
101        ProviderType::Storage => b"/tenzro/providers/storage".to_vec(),
102        ProviderType::Validator => b"/tenzro/providers/validator".to_vec(),
103    }
104}
105
106/// Provider types for discovery
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum ProviderType {
109    /// Inference/model providers
110    Inference,
111    /// TEE providers
112    Tee,
113    /// Storage providers
114    Storage,
115    /// Validator nodes
116    Validator,
117}
118
119impl ProviderType {
120    /// Converts NetworkRole to ProviderType
121    pub fn from_role(role: NetworkRole) -> Option<Self> {
122        match role {
123            NetworkRole::ModelProvider => Some(Self::Inference),
124            NetworkRole::TeeProvider => Some(Self::Tee),
125            NetworkRole::StorageProvider => Some(Self::Storage),
126            NetworkRole::Validator => Some(Self::Validator),
127            _ => None,
128        }
129    }
130
131    /// Converts to NetworkRole
132    pub fn to_role(self) -> NetworkRole {
133        match self {
134            Self::Inference => NetworkRole::ModelProvider,
135            Self::Tee => NetworkRole::TeeProvider,
136            Self::Storage => NetworkRole::StorageProvider,
137            Self::Validator => NetworkRole::Validator,
138        }
139    }
140}
141
142/// Bootstrap configuration for connecting to known boot nodes
143#[derive(Debug, Clone)]
144pub struct BootstrapConfig {
145    /// List of bootstrap node multiaddresses
146    pub boot_nodes: Vec<Multiaddr>,
147    /// Enable automatic reconnection to boot nodes on disconnect
148    pub enable_reconnect: bool,
149    /// Reconnection interval in seconds
150    pub reconnect_interval: Duration,
151}
152
153impl Default for BootstrapConfig {
154    fn default() -> Self {
155        Self {
156            boot_nodes: Vec::new(),
157            enable_reconnect: true,
158            reconnect_interval: Duration::from_secs(60),
159        }
160    }
161}
162
163impl BootstrapConfig {
164    /// Creates a new bootstrap config with the given boot nodes
165    pub fn new(boot_nodes: Vec<Multiaddr>) -> Self {
166        Self {
167            boot_nodes,
168            ..Default::default()
169        }
170    }
171
172    /// Creates a testnet bootstrap config
173    pub fn testnet() -> Self {
174        Self {
175            boot_nodes: vec![
176                "/dns4/testnet-boot-1.tenzro.network/tcp/9000".parse().unwrap(),
177                "/dns4/testnet-boot-2.tenzro.network/tcp/9000".parse().unwrap(),
178            ],
179            ..Default::default()
180        }
181    }
182
183    /// Creates a mainnet bootstrap config
184    pub fn mainnet() -> Self {
185        Self {
186            boot_nodes: vec![
187                "/dns4/mainnet-boot-1.tenzro.network/tcp/9000".parse().unwrap(),
188                "/dns4/mainnet-boot-2.tenzro.network/tcp/9000".parse().unwrap(),
189                "/dns4/mainnet-boot-3.tenzro.network/tcp/9000".parse().unwrap(),
190            ],
191            ..Default::default()
192        }
193    }
194}
195
196/// Discovery configuration
197#[derive(Debug, Clone)]
198pub struct DiscoveryConfig {
199    /// Enable random walk for discovering new peers
200    pub enable_random_walk: bool,
201    /// Random walk interval
202    pub random_walk_interval: Duration,
203    /// Enable provider announcements
204    pub enable_provider_announcement: bool,
205    /// Provider announcement interval
206    pub provider_announcement_interval: Duration,
207}
208
209impl Default for DiscoveryConfig {
210    fn default() -> Self {
211        Self {
212            enable_random_walk: true,
213            random_walk_interval: Duration::from_secs(300), // 5 minutes
214            enable_provider_announcement: false,
215            provider_announcement_interval: Duration::from_secs(600), // 10 minutes
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_kademlia_creation() {
226        let peer_id = PeerId::random();
227        let kad = create_kademlia(peer_id);
228        // Just verify it was created successfully
229        drop(kad);
230    }
231
232    #[test]
233    fn test_provider_keys() {
234        let inference_key = provider_key(ProviderType::Inference);
235        let tee_key = provider_key(ProviderType::Tee);
236
237        assert_ne!(inference_key, tee_key);
238        assert_eq!(inference_key, b"/tenzro/providers/inference");
239    }
240
241    #[test]
242    fn test_provider_type_conversion() {
243        assert_eq!(
244            ProviderType::from_role(NetworkRole::ModelProvider),
245            Some(ProviderType::Inference)
246        );
247        assert_eq!(
248            ProviderType::from_role(NetworkRole::FullNode),
249            None
250        );
251    }
252
253    #[test]
254    fn test_bootstrap_config_default() {
255        let config = BootstrapConfig::default();
256        assert!(config.boot_nodes.is_empty());
257        assert!(config.enable_reconnect);
258        assert_eq!(config.reconnect_interval, Duration::from_secs(60));
259    }
260
261    #[test]
262    fn test_bootstrap_config_testnet() {
263        let config = BootstrapConfig::testnet();
264        assert_eq!(config.boot_nodes.len(), 2);
265        assert!(config.enable_reconnect);
266    }
267
268    #[test]
269    fn test_bootstrap_config_mainnet() {
270        let config = BootstrapConfig::mainnet();
271        assert_eq!(config.boot_nodes.len(), 3);
272        assert!(config.enable_reconnect);
273    }
274
275    #[test]
276    fn test_connect_to_bootstrap_nodes() {
277        let peer_id = PeerId::random();
278        let mut kad = create_kademlia(peer_id);
279
280        // Create a boot node multiaddr with peer ID
281        let boot_peer = PeerId::random();
282        let boot_addr: Multiaddr = format!("/ip4/127.0.0.1/tcp/9000/p2p/{}", boot_peer)
283            .parse()
284            .unwrap();
285
286        let config = BootstrapConfig::new(vec![boot_addr.clone()]);
287        let addrs = connect_to_bootstrap_nodes(&mut kad, &config);
288
289        assert_eq!(addrs.len(), 1);
290        assert_eq!(addrs[0], boot_addr);
291    }
292
293    #[test]
294    fn test_connect_to_bootstrap_nodes_empty() {
295        let peer_id = PeerId::random();
296        let mut kad = create_kademlia(peer_id);
297
298        let config = BootstrapConfig::default();
299        let addrs = connect_to_bootstrap_nodes(&mut kad, &config);
300
301        assert!(addrs.is_empty());
302    }
303}