bittensor_rs/
registration.rs

1//! # Chain Registration
2//!
3//! Common chain registration functionality for neurons (miners and validators) to register
4//! on-chain and publish their axon endpoints for discovery.
5
6use crate::service::Service;
7use anyhow::Result;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{error, info, warn};
11
12/// Configuration for chain registration
13#[derive(Debug, Clone)]
14pub struct RegistrationConfig {
15    /// Network ID
16    pub netuid: u16,
17    /// Network name (e.g., "finney", "local")
18    pub network: String,
19    /// Axon port
20    pub axon_port: u16,
21    /// External IP (optional - will auto-detect if not provided)
22    pub external_ip: Option<String>,
23    /// Spoofed IP for local development (e.g., "10.0.0.1" for miners, "10.0.0.2" for validators)
24    pub local_spoofed_ip: String,
25    /// Neuron type for logging (e.g., "miner", "validator")
26    pub neuron_type: String,
27}
28
29/// Chain registration service for one-time startup registration
30///
31/// This service is used by both miners and validators to:
32/// 1. Register their presence on the Bittensor network
33/// 2. Publish their axon endpoint for discovery
34/// 3. Discover their assigned UID from the metagraph
35///
36/// The UID is dynamically discovered from the chain based on the neuron's hotkey,
37/// ensuring it's always accurate and up-to-date. UIDs should never be hardcoded
38/// in configuration files.
39#[derive(Clone)]
40pub struct ChainRegistration {
41    config: RegistrationConfig,
42    bittensor_service: Arc<Service>,
43    state: Arc<RwLock<RegistrationState>>,
44}
45
46/// Internal registration state
47#[derive(Debug)]
48struct RegistrationState {
49    /// Current registration status
50    is_registered: bool,
51    /// Registration timestamp
52    registration_time: Option<chrono::DateTime<chrono::Utc>>,
53    /// Discovered UID from metagraph
54    discovered_uid: Option<u16>,
55}
56
57/// Snapshot of the current registration state
58#[derive(Debug, Clone)]
59pub struct RegistrationStateSnapshot {
60    pub is_registered: bool,
61    pub registration_time: Option<chrono::DateTime<chrono::Utc>>,
62    pub discovered_uid: Option<u16>,
63}
64
65impl ChainRegistration {
66    /// Create a new chain registration service
67    pub fn new(config: RegistrationConfig, bittensor_service: Arc<Service>) -> Self {
68        info!(
69            "Initializing chain registration for {} on netuid: {}",
70            config.neuron_type, config.netuid
71        );
72
73        let state = Arc::new(RwLock::new(RegistrationState {
74            is_registered: false,
75            registration_time: None,
76            discovered_uid: None,
77        }));
78
79        Self {
80            config,
81            bittensor_service,
82            state,
83        }
84    }
85
86    /// Perform one-time startup registration
87    pub async fn register_startup(&self) -> Result<()> {
88        info!(
89            "Performing one-time startup chain registration for {}",
90            self.config.neuron_type
91        );
92
93        // First, check if our hotkey is registered in the metagraph
94        let our_account_id = self.bittensor_service.get_account_id();
95        info!(
96            "Checking registration for {} hotkey: {}",
97            self.config.neuron_type, our_account_id
98        );
99
100        // Get metagraph and use discovery to find our neuron
101        let metagraph = self
102            .bittensor_service
103            .get_metagraph(self.config.netuid)
104            .await
105            .map_err(|e| anyhow::anyhow!("Failed to get metagraph: {}", e))?;
106
107        let discovery = crate::discovery::NeuronDiscovery::new(&metagraph);
108        let our_neuron = discovery.find_neuron_by_hotkey(&our_account_id.to_string());
109
110        if let Some(neuron) = our_neuron {
111            // Update our state with the discovered UID
112            let mut state = self.state.write().await;
113            state.discovered_uid = Some(neuron.uid);
114            drop(state);
115
116            info!(
117                "Found {} hotkey registered with UID: {}",
118                self.config.neuron_type, neuron.uid
119            );
120
121            // Create the socket address for the axon
122            let axon_ip = self.determine_axon_ip().await?;
123            let axon_addr = format!("{}:{}", axon_ip, self.config.axon_port)
124                .parse()
125                .map_err(|e| anyhow::anyhow!("Invalid axon address: {}", e))?;
126
127            // Check if axon info has changed
128            let needs_update = if let Some(current_axon) = &neuron.axon_info {
129                let current_addr = &current_axon.socket_addr;
130                if current_addr != &axon_addr {
131                    info!(
132                        "Axon address has changed from {} to {} - updating registration",
133                        current_addr, axon_addr
134                    );
135                    true
136                } else {
137                    info!(
138                        "Axon already registered at {} - skipping serve_axon to avoid rate limiting",
139                        current_addr
140                    );
141                    false
142                }
143            } else {
144                info!(
145                    "No axon info found for neuron - registering new axon at {}",
146                    axon_addr
147                );
148                true
149            };
150
151            if needs_update {
152                info!(
153                    "Registering {} axon at address: {}",
154                    self.config.neuron_type, axon_addr
155                );
156
157                // Register axon once at startup
158                match self
159                    .bittensor_service
160                    .serve_axon(self.config.netuid, axon_addr)
161                    .await
162                {
163                    Ok(()) => {
164                        let mut state = self.state.write().await;
165                        state.is_registered = true;
166                        state.registration_time = Some(chrono::Utc::now());
167                        info!(
168                            "{} startup chain registration successful",
169                            self.config.neuron_type
170                        );
171                    }
172                    Err(e) => {
173                        // Check if this is a "Transaction is outdated" error
174                        let error_str = e.to_string();
175                        if error_str.contains("Transaction is outdated") {
176                            warn!(
177                                "Axon registration skipped - likely already registered at {}. Error: {}",
178                                axon_addr, e
179                            );
180                            // Mark as registered anyway since this error usually means it's already registered
181                            let mut state = self.state.write().await;
182                            state.is_registered = true;
183                            state.registration_time = Some(chrono::Utc::now());
184                        } else if error_str.contains("Custom error: 12") {
185                            // Handle rate limiting error specifically
186                            error!(
187                                "Rate limited when calling serve_axon (Custom error: 12). This typically happens when calling serve_axon too frequently. Current axon: {:?}, attempted: {}",
188                                neuron.axon_info, axon_addr
189                            );
190                            return Err(anyhow::anyhow!(
191                                "Rate limited when updating axon registration. Please wait before retrying."
192                            ));
193                        } else {
194                            error!(
195                                "{} startup chain registration failed: {}",
196                                self.config.neuron_type, e
197                            );
198                            return Err(anyhow::anyhow!(
199                                "Failed to register {} axon: {}",
200                                self.config.neuron_type,
201                                e
202                            ));
203                        }
204                    }
205                }
206            } else {
207                // Axon info hasn't changed, mark as registered without calling serve_axon
208                let mut state = self.state.write().await;
209                state.is_registered = true;
210                state.registration_time = Some(chrono::Utc::now());
211                info!(
212                    "{} registration completed - using existing axon endpoint",
213                    self.config.neuron_type
214                );
215            }
216        } else {
217            error!(
218                "Hotkey {} is not registered on subnet {} - please register your {} first",
219                our_account_id, self.config.netuid, self.config.neuron_type
220            );
221            return Err(anyhow::anyhow!(
222                "{} hotkey {} is not registered on subnet {}. Please register your {} using btcli before starting.",
223                self.config.neuron_type, our_account_id, self.config.netuid, self.config.neuron_type
224            ));
225        }
226
227        Ok(())
228    }
229
230    /// Determine the appropriate IP address for the axon
231    async fn determine_axon_ip(&self) -> Result<String> {
232        if let Some(external_ip) = &self.config.external_ip {
233            // Use configured external IP if provided
234            info!("Using configured external IP: {}", external_ip);
235            Ok(external_ip.clone())
236        } else if self.config.network == "local" {
237            // For local development, use a private network IP that will pass chain validation
238            warn!(
239                "Using spoofed IP {} for local development - axon won't be reachable at this address",
240                self.config.local_spoofed_ip
241            );
242            Ok(self.config.local_spoofed_ip.clone())
243        } else {
244            // For production, try to auto-detect public IP
245            info!("No external_ip configured, attempting to auto-detect public IP address...");
246
247            match Self::detect_public_ip().await {
248                Some(ip) => {
249                    info!("Auto-detected public IP: {}", ip);
250                    warn!(
251                        "Using auto-detected IP {}. For production, consider setting external_ip in configuration for reliability",
252                        ip
253                    );
254                    Ok(ip)
255                }
256                None => {
257                    error!("Failed to auto-detect public IP address");
258                    Err(anyhow::anyhow!(
259                        "Could not auto-detect public IP address. Please set external_ip in configuration."
260                    ))
261                }
262            }
263        }
264    }
265
266    /// Attempt to detect the public IP address
267    ///
268    /// Uses multiple methods to try to determine the public IP:
269    /// 1. Try curl to ipinfo.io
270    /// 2. Try curl to ifconfig.me
271    /// 3. Return None if all methods fail
272    async fn detect_public_ip() -> Option<String> {
273        // Try ipinfo.io first
274        if let Ok(output) = tokio::process::Command::new("curl")
275            .args(["-s", "-m", "5", "https://ipinfo.io/ip"])
276            .output()
277            .await
278        {
279            if output.status.success() {
280                let ip = String::from_utf8_lossy(&output.stdout).trim().to_string();
281                if Self::is_valid_ip(&ip) {
282                    return Some(ip);
283                }
284            }
285        }
286
287        // Try ifconfig.me as fallback
288        if let Ok(output) = tokio::process::Command::new("curl")
289            .args(["-s", "-m", "5", "https://ifconfig.me"])
290            .output()
291            .await
292        {
293            if output.status.success() {
294                let ip = String::from_utf8_lossy(&output.stdout).trim().to_string();
295                if Self::is_valid_ip(&ip) {
296                    return Some(ip);
297                }
298            }
299        }
300
301        None
302    }
303
304    /// Check if a string is a valid IPv4 address
305    fn is_valid_ip(ip: &str) -> bool {
306        ip.parse::<std::net::Ipv4Addr>().is_ok()
307    }
308
309    /// Get current registration state
310    pub async fn get_state(&self) -> RegistrationStateSnapshot {
311        let state = self.state.read().await;
312        RegistrationStateSnapshot {
313            is_registered: state.is_registered,
314            registration_time: state.registration_time,
315            discovered_uid: state.discovered_uid,
316        }
317    }
318
319    /// Get the UID discovered from the metagraph
320    ///
321    /// Returns the neuron's UID as discovered from the chain based on its hotkey.
322    /// This is the authoritative source for a neuron's UID and should be used
323    /// instead of any hardcoded configuration values.
324    ///
325    /// Returns `None` if:
326    /// - The neuron hasn't been registered yet
327    /// - Registration is skipped (local testing mode)
328    /// - The hotkey wasn't found in the metagraph
329    pub async fn get_discovered_uid(&self) -> Option<u16> {
330        self.state.read().await.discovered_uid
331    }
332
333    /// Health check for registration service
334    pub async fn health_check(&self) -> Result<()> {
335        let state = self.state.read().await;
336
337        if !state.is_registered {
338            return Err(anyhow::anyhow!("Chain registration not completed"));
339        }
340
341        // Check if registration is too old (warn but don't fail)
342        if let Some(reg_time) = state.registration_time {
343            let elapsed = chrono::Utc::now().signed_duration_since(reg_time);
344            if elapsed > chrono::Duration::hours(24) {
345                warn!(
346                    "Chain registration is old (registered {} hours ago)",
347                    elapsed.num_hours()
348                );
349            }
350        }
351
352        Ok(())
353    }
354}
355
356/// Builder for RegistrationConfig
357pub struct RegistrationConfigBuilder {
358    netuid: u16,
359    network: String,
360    axon_port: u16,
361    external_ip: Option<String>,
362    local_spoofed_ip: String,
363    neuron_type: String,
364}
365
366impl RegistrationConfigBuilder {
367    pub fn new(netuid: u16, network: String, axon_port: u16) -> Self {
368        Self {
369            netuid,
370            network,
371            axon_port,
372            external_ip: None,
373            local_spoofed_ip: "10.0.0.1".to_string(),
374            neuron_type: "neuron".to_string(),
375        }
376    }
377
378    pub fn external_ip(mut self, ip: Option<String>) -> Self {
379        self.external_ip = ip;
380        self
381    }
382
383    pub fn local_spoofed_ip(mut self, ip: String) -> Self {
384        self.local_spoofed_ip = ip;
385        self
386    }
387
388    pub fn neuron_type(mut self, neuron_type: String) -> Self {
389        self.neuron_type = neuron_type;
390        self
391    }
392
393    pub fn build(self) -> RegistrationConfig {
394        RegistrationConfig {
395            netuid: self.netuid,
396            network: self.network,
397            axon_port: self.axon_port,
398            external_ip: self.external_ip,
399            local_spoofed_ip: self.local_spoofed_ip,
400            neuron_type: self.neuron_type,
401        }
402    }
403}