bittensor_rs/
discovery.rs

1//! # Neuron Discovery
2//!
3//! Provides functionality to discover neurons (miners and validators) from the Bittensor metagraph,
4//! including their registration status and axon endpoints.
5
6use anyhow::Result;
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use tracing::{debug, info, warn};
10
11use crate::Metagraph;
12
13/// Information about a discovered neuron
14#[derive(Debug, Clone)]
15pub struct NeuronInfo {
16    /// The neuron's unique identifier
17    pub uid: u16,
18    /// The neuron's hotkey (SS58 address)
19    pub hotkey: String,
20    /// The neuron's coldkey (SS58 address)
21    pub coldkey: String,
22    /// The neuron's stake amount
23    pub stake: u64,
24    /// Whether this neuron is a validator
25    pub is_validator: bool,
26    /// The neuron's axon endpoint (if published)
27    pub axon_info: Option<AxonInfo>,
28}
29
30/// Axon endpoint information
31#[derive(Debug, Clone)]
32pub struct AxonInfo {
33    /// IP address
34    pub ip: String,
35    /// Port number
36    pub port: u16,
37    /// Protocol version
38    pub version: u32,
39    /// Full socket address
40    pub socket_addr: SocketAddr,
41}
42
43/// Discovers neurons from the metagraph
44pub struct NeuronDiscovery<'a> {
45    metagraph: &'a Metagraph,
46}
47
48impl<'a> NeuronDiscovery<'a> {
49    /// Create a new neuron discovery instance
50    pub fn new(metagraph: &'a Metagraph) -> Self {
51        Self { metagraph }
52    }
53
54    /// Get all neurons with their information
55    pub fn get_all_neurons(&self) -> Result<Vec<NeuronInfo>> {
56        let mut neurons = Vec::new();
57
58        // Iterate through all UIDs based on hotkeys length
59        for (idx, hotkey) in self.metagraph.hotkeys.iter().enumerate() {
60            let uid = idx as u16;
61
62            if let Some(axon_info) = self.extract_axon_info(uid) {
63                debug!(
64                    "Found neuron UID {} with axon endpoint {}:{}",
65                    uid, axon_info.ip, axon_info.port
66                );
67            }
68
69            // Get corresponding data from parallel arrays
70            let coldkey = self
71                .metagraph
72                .coldkeys
73                .get(idx)
74                .map(|c| c.to_string())
75                .unwrap_or_default();
76
77            let stake = self
78                .metagraph
79                .total_stake
80                .get(idx)
81                .map(|s| s.0)
82                .unwrap_or(0);
83
84            let is_validator = self
85                .metagraph
86                .validator_permit
87                .get(idx)
88                .copied()
89                .unwrap_or(false);
90
91            neurons.push(NeuronInfo {
92                uid,
93                hotkey: hotkey.to_string(),
94                coldkey,
95                stake,
96                is_validator,
97                axon_info: self.extract_axon_info(uid),
98            });
99        }
100
101        info!("Discovered {} neurons from metagraph", neurons.len());
102        Ok(neurons)
103    }
104
105    /// Get only validators (neurons with validator permit)
106    pub fn get_validators(&self) -> Result<Vec<NeuronInfo>> {
107        let all_neurons = self.get_all_neurons()?;
108        let validators: Vec<NeuronInfo> =
109            all_neurons.into_iter().filter(|n| n.is_validator).collect();
110
111        info!("Found {} validators in metagraph", validators.len());
112        Ok(validators)
113    }
114
115    /// Get only miners (neurons without validator permit)
116    pub fn get_miners(&self) -> Result<Vec<NeuronInfo>> {
117        let all_neurons = self.get_all_neurons()?;
118        let miners: Vec<NeuronInfo> = all_neurons
119            .into_iter()
120            .filter(|n| !n.is_validator)
121            .collect();
122
123        info!("Found {} miners in metagraph", miners.len());
124        Ok(miners)
125    }
126
127    /// Get neurons with published axon endpoints
128    pub fn get_neurons_with_axons(&self) -> Result<Vec<NeuronInfo>> {
129        let all_neurons = self.get_all_neurons()?;
130        let with_axons: Vec<NeuronInfo> = all_neurons
131            .into_iter()
132            .filter(|n| n.axon_info.is_some())
133            .collect();
134
135        info!("Found {} neurons with axon endpoints", with_axons.len());
136        Ok(with_axons)
137    }
138
139    /// Find a specific neuron by hotkey
140    pub fn find_neuron_by_hotkey(&self, hotkey: &str) -> Option<NeuronInfo> {
141        for (idx, h) in self.metagraph.hotkeys.iter().enumerate() {
142            if h.to_string() == hotkey {
143                let uid = idx as u16;
144                let coldkey = self
145                    .metagraph
146                    .coldkeys
147                    .get(idx)
148                    .map(|c| c.to_string())
149                    .unwrap_or_default();
150
151                let stake = self
152                    .metagraph
153                    .total_stake
154                    .get(idx)
155                    .map(|s| s.0)
156                    .unwrap_or(0);
157
158                let is_validator = self
159                    .metagraph
160                    .validator_permit
161                    .get(idx)
162                    .copied()
163                    .unwrap_or(false);
164
165                return Some(NeuronInfo {
166                    uid,
167                    hotkey: h.to_string(),
168                    coldkey,
169                    stake,
170                    is_validator,
171                    axon_info: self.extract_axon_info(uid),
172                });
173            }
174        }
175        None
176    }
177
178    /// Find a specific neuron by UID
179    pub fn find_neuron_by_uid(&self, uid: u16) -> Option<NeuronInfo> {
180        let idx = uid as usize;
181        if idx >= self.metagraph.hotkeys.len() {
182            return None;
183        }
184
185        let hotkey = self.metagraph.hotkeys.get(idx)?;
186        let coldkey = self
187            .metagraph
188            .coldkeys
189            .get(idx)
190            .map(|c| c.to_string())
191            .unwrap_or_default();
192
193        let stake = self
194            .metagraph
195            .total_stake
196            .get(idx)
197            .map(|s| s.0)
198            .unwrap_or(0);
199
200        let is_validator = self
201            .metagraph
202            .validator_permit
203            .get(idx)
204            .copied()
205            .unwrap_or(false);
206
207        Some(NeuronInfo {
208            uid,
209            hotkey: hotkey.to_string(),
210            coldkey,
211            stake,
212            is_validator,
213            axon_info: self.extract_axon_info(uid),
214        })
215    }
216
217    /// Check if a hotkey is registered in the metagraph
218    pub fn is_hotkey_registered(&self, hotkey: &str) -> bool {
219        self.find_neuron_by_hotkey(hotkey).is_some()
220    }
221
222    /// Get neurons filtered by minimum stake
223    pub fn get_neurons_by_min_stake(&self, min_stake: u64) -> Result<Vec<NeuronInfo>> {
224        let all_neurons = self.get_all_neurons()?;
225        let filtered: Vec<NeuronInfo> = all_neurons
226            .into_iter()
227            .filter(|n| n.stake >= min_stake)
228            .collect();
229
230        info!(
231            "Found {} neurons with stake >= {}",
232            filtered.len(),
233            min_stake
234        );
235        Ok(filtered)
236    }
237
238    /// Extract axon information for a specific UID
239    pub fn extract_axon_info(&self, uid: u16) -> Option<AxonInfo> {
240        self.metagraph.axons.get(uid as usize).and_then(|axon| {
241            // Check if axon has valid IP and port
242            if axon.ip == 0 || axon.port == 0 {
243                return None;
244            }
245
246            // Convert IP to string format based on IP type
247            let ip_str = if axon.ip_type == 4 {
248                // IPv4 addresses are stored in the lower 32 bits of the u128
249                let ipv4_bits = axon.ip as u32;
250                let ip_bytes = ipv4_bits.to_be_bytes();
251                format!(
252                    "{}.{}.{}.{}",
253                    ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
254                )
255            } else {
256                // IPv6 handling - full u128
257                format!("{:x}", axon.ip)
258            };
259
260            // Validate IP address
261            if ip_str == "0.0.0.0" || ip_str == "127.0.0.1" {
262                debug!("Skipping invalid axon IP {} for UID {}", ip_str, uid);
263                return None;
264            }
265
266            // Create socket address
267            match format!("{}:{}", ip_str, axon.port).parse::<SocketAddr>() {
268                Ok(socket_addr) => Some(AxonInfo {
269                    ip: ip_str,
270                    port: axon.port,
271                    version: axon.version,
272                    socket_addr,
273                }),
274                Err(e) => {
275                    warn!(
276                        "Failed to parse socket address for UID {}: {}:{} - {}",
277                        uid, ip_str, axon.port, e
278                    );
279                    None
280                }
281            }
282        })
283    }
284}
285
286/// Create a mapping of hotkeys to UIDs for quick lookup
287pub fn create_hotkey_to_uid_map(metagraph: &Metagraph) -> HashMap<String, u16> {
288    let mut map = HashMap::new();
289    for (idx, hotkey) in metagraph.hotkeys.iter().enumerate() {
290        map.insert(hotkey.to_string(), idx as u16);
291    }
292    map
293}
294
295/// Create a mapping of UIDs to axon endpoints
296pub fn create_uid_to_axon_map(metagraph: &Metagraph) -> HashMap<u16, SocketAddr> {
297    let discovery = NeuronDiscovery::new(metagraph);
298    let mut map = HashMap::new();
299
300    for idx in 0..metagraph.hotkeys.len() {
301        let uid = idx as u16;
302        if let Some(axon_info) = discovery.extract_axon_info(uid) {
303            map.insert(uid, axon_info.socket_addr);
304        }
305    }
306    map
307}
308
309#[cfg(test)]
310mod tests {
311
312    #[test]
313    fn test_ip_conversion() {
314        // Test IP address conversion from u32 to string
315        let ip_u32: u32 = 0xC0A80101; // 192.168.1.1
316        let ip_bytes = ip_u32.to_be_bytes();
317        let ip_str = format!(
318            "{}.{}.{}.{}",
319            ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
320        );
321        assert_eq!(ip_str, "192.168.1.1");
322    }
323}