1use crate::service::Service;
7use anyhow::Result;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{error, info, warn};
11
12#[derive(Debug, Clone)]
14pub struct RegistrationConfig {
15 pub netuid: u16,
17 pub network: String,
19 pub axon_port: u16,
21 pub external_ip: Option<String>,
23 pub local_spoofed_ip: String,
25 pub neuron_type: String,
27}
28
29#[derive(Clone)]
40pub struct ChainRegistration {
41 config: RegistrationConfig,
42 bittensor_service: Arc<Service>,
43 state: Arc<RwLock<RegistrationState>>,
44}
45
46#[derive(Debug)]
48struct RegistrationState {
49 is_registered: bool,
51 registration_time: Option<chrono::DateTime<chrono::Utc>>,
53 discovered_uid: Option<u16>,
55}
56
57#[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 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 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 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 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 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 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 let needs_update = if let Some(current_axon) = &neuron.axon_info {
129 let current_addr = ¤t_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 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 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 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 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 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 async fn determine_axon_ip(&self) -> Result<String> {
232 if let Some(external_ip) = &self.config.external_ip {
233 info!("Using configured external IP: {}", external_ip);
235 Ok(external_ip.clone())
236 } else if self.config.network == "local" {
237 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 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 async fn detect_public_ip() -> Option<String> {
273 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 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 fn is_valid_ip(ip: &str) -> bool {
306 ip.parse::<std::net::Ipv4Addr>().is_ok()
307 }
308
309 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 pub async fn get_discovered_uid(&self) -> Option<u16> {
330 self.state.read().await.discovered_uid
331 }
332
333 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 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
356pub 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}