1use crate::error::{LightningError, Result};
2use crate::registry::MinerRegistry;
3use crate::signing::Signer;
4use crate::types::{
5 handshake_request_message, handshake_response_message, read_frame, write_frame_and_finish,
6 HandshakeRequest, HandshakeResponse, MessageType, PeerAddr, QuicAxonInfo, QuicRequest,
7 QuicResponse, StreamChunk, StreamEnd, SynapsePacket, SynapseResponse,
8 DEFAULT_MAX_FRAME_PAYLOAD,
9};
10use crate::util::unix_timestamp_secs;
11use base64::{prelude::BASE64_STANDARD, Engine};
12use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig};
13use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
14use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
15use rustls::ClientConfig as RustlsClientConfig;
16use sp_core::blake2_256;
17use std::collections::HashMap;
18use std::net::SocketAddr;
19use std::sync::Arc;
20use std::time::Duration;
21use tokio::sync::RwLock;
22use tokio::time::Instant;
23use tracing::{debug, error, info, instrument, warn};
24
25#[cfg(feature = "subtensor")]
26use crate::metagraph::{connect_subtensor, Metagraph, MetagraphMonitorConfig};
27#[cfg(feature = "subtensor")]
28use subxt::PolkadotConfig;
29
30#[derive(Clone)]
34pub struct LightningClientConfig {
35 pub connect_timeout: Duration,
37 pub idle_timeout: Duration,
39 pub keep_alive_interval: Duration,
41 pub reconnect_initial_backoff: Duration,
43 pub reconnect_max_backoff: Duration,
45 pub reconnect_max_retries: u32,
47 pub reconnect_slow_probe_interval: Option<Duration>,
51 pub max_connections: usize,
53 pub max_frame_payload_bytes: usize,
55 pub max_stream_payload_bytes: usize,
59 pub stream_chunk_timeout: Option<Duration>,
62 #[cfg(feature = "subtensor")]
65 pub metagraph: Option<MetagraphMonitorConfig>,
66}
67
68impl Default for LightningClientConfig {
69 fn default() -> Self {
70 Self {
71 connect_timeout: Duration::from_secs(10),
72 idle_timeout: Duration::from_secs(150),
73 keep_alive_interval: Duration::from_secs(30),
74 reconnect_initial_backoff: Duration::from_secs(1),
75 reconnect_max_backoff: Duration::from_secs(60),
76 reconnect_max_retries: 5,
77 reconnect_slow_probe_interval: Some(Duration::from_secs(60)),
78 max_connections: 1024,
79 max_frame_payload_bytes: DEFAULT_MAX_FRAME_PAYLOAD,
80 max_stream_payload_bytes: DEFAULT_MAX_FRAME_PAYLOAD,
81 stream_chunk_timeout: None,
82 #[cfg(feature = "subtensor")]
83 metagraph: None,
84 }
85 }
86}
87
88impl LightningClientConfig {
89 pub fn builder() -> LightningClientConfigBuilder {
90 LightningClientConfigBuilder {
91 config: Self::default(),
92 }
93 }
94
95 fn validate(&self) -> Result<()> {
96 if self.connect_timeout.is_zero() {
97 return Err(LightningError::Config(
98 "connect_timeout must be non-zero".into(),
99 ));
100 }
101 if self.idle_timeout.is_zero() {
102 return Err(LightningError::Config(
103 "idle_timeout must be non-zero".into(),
104 ));
105 }
106 if self.keep_alive_interval.is_zero() {
107 return Err(LightningError::Config(
108 "keep_alive_interval must be non-zero".into(),
109 ));
110 }
111 if self.keep_alive_interval >= self.idle_timeout {
112 return Err(LightningError::Config(format!(
113 "keep_alive_interval ({:?}) must be less than idle_timeout ({:?})",
114 self.keep_alive_interval, self.idle_timeout
115 )));
116 }
117 if self.reconnect_initial_backoff.is_zero() {
118 return Err(LightningError::Config(
119 "reconnect_initial_backoff must be non-zero".into(),
120 ));
121 }
122 if self.reconnect_max_backoff.is_zero() {
123 return Err(LightningError::Config(
124 "reconnect_max_backoff must be non-zero".into(),
125 ));
126 }
127 if self.reconnect_initial_backoff > self.reconnect_max_backoff {
128 return Err(LightningError::Config(format!(
129 "reconnect_initial_backoff ({:?}) must be <= reconnect_max_backoff ({:?})",
130 self.reconnect_initial_backoff, self.reconnect_max_backoff
131 )));
132 }
133 if self.reconnect_max_retries == 0 {
134 return Err(LightningError::Config(
135 "reconnect_max_retries must be at least 1".into(),
136 ));
137 }
138 if self
139 .reconnect_slow_probe_interval
140 .is_some_and(|d| d.is_zero())
141 {
142 return Err(LightningError::Config(
143 "reconnect_slow_probe_interval must be non-zero when set".into(),
144 ));
145 }
146 if self.max_connections == 0 {
147 return Err(LightningError::Config(
148 "max_connections must be at least 1".into(),
149 ));
150 }
151 if self.max_frame_payload_bytes < 1_048_576 {
152 return Err(LightningError::Config(format!(
153 "max_frame_payload_bytes ({}) must be at least 1048576 (1 MB)",
154 self.max_frame_payload_bytes
155 )));
156 }
157 if self.max_frame_payload_bytes > u32::MAX as usize {
158 return Err(LightningError::Config(format!(
159 "max_frame_payload_bytes ({}) must not exceed {} (u32::MAX)",
160 self.max_frame_payload_bytes,
161 u32::MAX
162 )));
163 }
164 if self.stream_chunk_timeout.is_some_and(|d| d.is_zero()) {
165 return Err(LightningError::Config(
166 "stream_chunk_timeout must be non-zero".into(),
167 ));
168 }
169 if self.max_stream_payload_bytes < self.max_frame_payload_bytes {
170 return Err(LightningError::Config(format!(
171 "max_stream_payload_bytes ({}) must be >= max_frame_payload_bytes ({})",
172 self.max_stream_payload_bytes, self.max_frame_payload_bytes
173 )));
174 }
175 Ok(())
176 }
177}
178
179pub struct LightningClientConfigBuilder {
180 config: LightningClientConfig,
181}
182
183impl LightningClientConfigBuilder {
184 pub fn connect_timeout(mut self, val: Duration) -> Self {
185 self.config.connect_timeout = val;
186 self
187 }
188 pub fn idle_timeout(mut self, val: Duration) -> Self {
189 self.config.idle_timeout = val;
190 self
191 }
192 pub fn keep_alive_interval(mut self, val: Duration) -> Self {
193 self.config.keep_alive_interval = val;
194 self
195 }
196 pub fn reconnect_initial_backoff(mut self, val: Duration) -> Self {
197 self.config.reconnect_initial_backoff = val;
198 self
199 }
200 pub fn reconnect_max_backoff(mut self, val: Duration) -> Self {
201 self.config.reconnect_max_backoff = val;
202 self
203 }
204 pub fn reconnect_max_retries(mut self, val: u32) -> Self {
205 self.config.reconnect_max_retries = val;
206 self
207 }
208 pub fn reconnect_slow_probe_interval(mut self, val: Option<Duration>) -> Self {
209 self.config.reconnect_slow_probe_interval = val;
210 self
211 }
212 pub fn max_connections(mut self, val: usize) -> Self {
213 self.config.max_connections = val;
214 self
215 }
216 pub fn max_frame_payload_bytes(mut self, val: usize) -> Self {
217 self.config.max_frame_payload_bytes = val;
218 self
219 }
220 pub fn max_stream_payload_bytes(mut self, val: usize) -> Self {
221 self.config.max_stream_payload_bytes = val;
222 self
223 }
224 pub fn stream_chunk_timeout(mut self, val: Duration) -> Self {
225 self.config.stream_chunk_timeout = Some(val);
226 self
227 }
228 #[cfg(feature = "subtensor")]
229 pub fn metagraph(mut self, val: MetagraphMonitorConfig) -> Self {
230 self.config.metagraph = Some(val);
231 self
232 }
233 pub fn build(self) -> Result<LightningClientConfig> {
234 self.config.validate()?;
235 Ok(self.config)
236 }
237}
238
239struct ClientState {
240 registry: MinerRegistry,
241 #[cfg(feature = "subtensor")]
242 metagraph_shutdown: Option<tokio::sync::watch::Sender<bool>>,
243 #[cfg(feature = "subtensor")]
244 metagraph_handle: Option<tokio::task::JoinHandle<()>>,
245}
246
247pub struct StreamingResponse {
252 recv: quinn::RecvStream,
253 max_payload: usize,
254 max_stream_payload: usize,
255 chunk_timeout: Option<Duration>,
256}
257
258impl StreamingResponse {
259 pub async fn next_chunk(&mut self) -> Result<Option<Vec<u8>>> {
261 let frame_result = match self.chunk_timeout {
262 Some(timeout) => {
263 match tokio::time::timeout(timeout, read_frame(&mut self.recv, self.max_payload))
264 .await
265 {
266 Ok(r) => r,
267 Err(_) => {
268 self.recv.stop(0u32.into()).ok();
269 return Err(LightningError::Stream("chunk read timed out".to_string()));
270 }
271 }
272 }
273 None => read_frame(&mut self.recv, self.max_payload).await,
274 };
275 match frame_result {
276 Ok((MessageType::StreamChunk, payload)) => {
277 let chunk: StreamChunk = rmp_serde::from_slice(&payload).map_err(|e| {
278 LightningError::Serialization(format!("Failed to parse stream chunk: {}", e))
279 })?;
280 Ok(Some(chunk.data))
281 }
282 Ok((MessageType::StreamEnd, payload)) => {
283 let end: StreamEnd = rmp_serde::from_slice(&payload).map_err(|e| {
284 LightningError::Serialization(format!("Failed to parse stream end: {}", e))
285 })?;
286 if end.success {
287 Ok(None)
288 } else {
289 Err(LightningError::Stream(end.error.unwrap_or_else(|| {
290 "stream ended with failure status".to_string()
291 })))
292 }
293 }
294 Ok((MessageType::SynapseResponse, payload)) => {
295 let detail = rmp_serde::from_slice::<SynapseResponse>(&payload)
296 .ok()
297 .and_then(|r| r.error)
298 .unwrap_or_else(|| "no detail".to_string());
299 Err(LightningError::Stream(format!(
300 "server returned SynapseResponse error on streaming path: {}",
301 detail
302 )))
303 }
304 Ok((msg_type, _)) => Err(LightningError::Stream(format!(
305 "unexpected message type during streaming: {:?}",
306 msg_type
307 ))),
308 Err(e) => Err(e),
309 }
310 }
311
312 pub async fn collect_all(&mut self) -> Result<Vec<Vec<u8>>> {
314 let mut chunks = Vec::new();
315 let mut total_size: usize = 0;
316 while let Some(chunk) = self.next_chunk().await? {
317 total_size = total_size.checked_add(chunk.len()).ok_or_else(|| {
318 LightningError::Stream("streaming response size overflow".to_string())
319 })?;
320 if total_size > self.max_stream_payload {
321 return Err(LightningError::Stream(format!(
322 "streaming response exceeded {} byte aggregate limit",
323 self.max_stream_payload
324 )));
325 }
326 chunks.push(chunk);
327 }
328 Ok(chunks)
329 }
330}
331
332pub struct LightningClient {
338 config: LightningClientConfig,
339 wallet_hotkey: String,
340 signer: Option<Arc<dyn Signer>>,
341 state: Arc<RwLock<ClientState>>,
342 endpoint: Option<Endpoint>,
343}
344
345impl LightningClient {
346 pub fn new(wallet_hotkey: String) -> Self {
348 Self::with_config(wallet_hotkey, LightningClientConfig::default())
349 .expect("default config is always valid")
350 }
351
352 pub fn with_config(wallet_hotkey: String, config: LightningClientConfig) -> Result<Self> {
354 config.validate()?;
355 Ok(Self {
356 config,
357 wallet_hotkey,
358 signer: None,
359 state: Arc::new(RwLock::new(ClientState {
360 registry: MinerRegistry::new(),
361 #[cfg(feature = "subtensor")]
362 metagraph_shutdown: None,
363 #[cfg(feature = "subtensor")]
364 metagraph_handle: None,
365 })),
366 endpoint: None,
367 })
368 }
369
370 pub fn set_signer(&mut self, signer: Box<dyn Signer>) {
373 self.signer = Some(Arc::from(signer));
374 info!("Signer configured");
375 }
376
377 #[cfg(feature = "btwallet")]
379 pub fn set_wallet(
380 &mut self,
381 wallet_name: &str,
382 wallet_path: &str,
383 hotkey_name: &str,
384 ) -> Result<()> {
385 let signer =
386 crate::signing::BtWalletSigner::from_wallet(wallet_name, wallet_path, hotkey_name)?;
387 self.set_signer(Box::new(signer));
388 Ok(())
389 }
390
391 #[instrument(skip(self, miners), fields(miner_count = miners.len()))]
396 pub async fn initialize_connections(&mut self, miners: Vec<QuicAxonInfo>) -> Result<()> {
397 self.create_endpoint().await?;
398
399 let endpoint = self
400 .endpoint
401 .as_ref()
402 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
403 .clone();
404 let wallet_hotkey = self.wallet_hotkey.clone();
405 let signer = self
406 .signer
407 .as_ref()
408 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
409 .clone();
410 let timeout = self.config.connect_timeout;
411
412 let mut addr_groups: HashMap<PeerAddr, Vec<QuicAxonInfo>> = HashMap::new();
413 for miner in miners {
414 addr_groups.entry(miner.addr_key()).or_default().push(miner);
415 }
416
417 let (active_count, remaining_capacity) = {
418 let state = self.state.read().await;
419 let active = state.registry.connection_count();
420 (active, self.config.max_connections.saturating_sub(active))
421 };
422
423 let addr_groups: Vec<(PeerAddr, Vec<QuicAxonInfo>)> =
424 if addr_groups.len() > remaining_capacity {
425 warn!(
426 "Connection limit ({}) reached with {} active, skipping {} of {} new addresses",
427 self.config.max_connections,
428 active_count,
429 addr_groups.len() - remaining_capacity,
430 addr_groups.len()
431 );
432 addr_groups.into_iter().take(remaining_capacity).collect()
433 } else {
434 addr_groups.into_iter().collect()
435 };
436
437 let max_fp = self.config.max_frame_payload_bytes;
438 let mut set = tokio::task::JoinSet::new();
439 for (addr_key, miners_at_addr) in addr_groups {
440 let ep = endpoint.clone();
441 let wh = wallet_hotkey.clone();
442 let s = signer.clone();
443 set.spawn(connect_and_authenticate_per_address(
444 ep,
445 wh,
446 s,
447 addr_key,
448 miners_at_addr,
449 timeout,
450 max_fp,
451 ));
452 }
453
454 let mut results = Vec::new();
455 while let Some(join_result) = set.join_next().await {
456 match join_result {
457 Ok((addr_key, conn_result, authenticated)) => {
458 results.push((addr_key, conn_result, authenticated));
459 }
460 Err(e) => {
461 error!("Connection task panicked: {}", e);
462 }
463 }
464 }
465
466 let mut state = self.state.write().await;
467 for (addr_key, conn_result, authenticated) in results {
468 match conn_result {
469 Ok(connection) => {
470 if authenticated.is_empty() {
471 warn!(
472 "No hotkeys authenticated at {}, dropping connection",
473 addr_key
474 );
475 connection.close(0u32.into(), b"no_authenticated_hotkeys");
476 } else {
477 for miner in authenticated {
478 info!("Authenticated miner {} at {}", miner.hotkey, addr_key);
479 state.registry.register(miner);
480 }
481 state.registry.set_connection(addr_key, connection);
482 }
483 }
484 Err(e) => {
485 error!("Failed to connect to {}: {}", addr_key, e);
486 }
487 }
488 }
489
490 #[cfg(feature = "subtensor")]
491 if let Some(metagraph_config) = self.config.metagraph.clone() {
492 self.start_metagraph_monitor(metagraph_config).await?;
493 }
494
495 Ok(())
496 }
497
498 #[instrument(skip(self))]
501 pub async fn create_endpoint(&mut self) -> Result<()> {
502 let mut tls_config = RustlsClientConfig::builder_with_provider(
503 rustls::crypto::ring::default_provider().into(),
504 )
505 .with_safe_default_protocol_versions()
506 .map_err(|e| LightningError::Config(format!("Failed to set TLS versions: {}", e)))?
507 .dangerous()
508 .with_custom_certificate_verifier(Arc::new(AcceptAnyCertVerifier))
509 .with_no_client_auth();
510
511 tls_config.alpn_protocols = vec![b"btlightning".to_vec()];
512
513 let mut transport_config = TransportConfig::default();
514
515 let idle_timeout = IdleTimeout::try_from(self.config.idle_timeout)
516 .map_err(|e| LightningError::Config(format!("Failed to set idle timeout: {}", e)))?;
517 transport_config.max_idle_timeout(Some(idle_timeout));
518 transport_config.keep_alive_interval(Some(self.config.keep_alive_interval));
519
520 let quic_crypto =
521 quinn::crypto::rustls::QuicClientConfig::try_from(tls_config).map_err(|e| {
522 LightningError::Config(format!("Failed to create QUIC crypto config: {}", e))
523 })?;
524 let mut client_config = ClientConfig::new(Arc::new(quic_crypto));
525 client_config.transport_config(Arc::new(transport_config));
526
527 let bind_addr: SocketAddr = "0.0.0.0:0"
528 .parse()
529 .map_err(|e| LightningError::Config(format!("Failed to parse bind address: {}", e)))?;
530 let mut endpoint = Endpoint::client(bind_addr).map_err(|e| {
531 LightningError::Connection(format!("Failed to create QUIC endpoint: {}", e))
532 })?;
533 endpoint.set_default_client_config(client_config);
534 self.endpoint = Some(endpoint);
535
536 info!("QUIC client endpoint created");
537 Ok(())
538 }
539
540 #[instrument(skip(self, axon_info, request), fields(miner_ip = %axon_info.ip, miner_port = axon_info.port))]
544 pub async fn query_axon(
545 &self,
546 axon_info: QuicAxonInfo,
547 request: QuicRequest,
548 ) -> Result<QuicResponse> {
549 self.query_axon_inner(axon_info, request, None).await
550 }
551
552 async fn query_axon_inner(
553 &self,
554 axon_info: QuicAxonInfo,
555 request: QuicRequest,
556 response_timeout: Option<Duration>,
557 ) -> Result<QuicResponse> {
558 let addr_key = axon_info.addr_key();
559
560 let (connection, bound) = {
561 let state = self.state.read().await;
562 (
563 state.registry.get_connection(&addr_key),
564 state
565 .registry
566 .is_authenticated_at(&axon_info.hotkey, &addr_key),
567 )
568 };
569
570 let max_fp = self.config.max_frame_payload_bytes;
571 match connection {
572 Some(conn) if conn.close_reason().is_none() => {
573 if !bound {
574 return Err(LightningError::Handshake(format!(
575 "no authenticated route for {} at {}",
576 axon_info.hotkey, addr_key
577 )));
578 }
579 debug!(
580 addr = %addr_key,
581 stable_id = conn.stable_id(),
582 "query_axon: connection alive, sending synapse"
583 );
584 send_synapse_packet(&conn, request, max_fp, response_timeout).await
585 }
586 Some(conn) => {
587 let reason = conn.close_reason();
588 warn!(
589 addr = %addr_key,
590 stable_id = conn.stable_id(),
591 close_reason = ?reason,
592 "QUIC connection closed, triggering reconnect"
593 );
594 self.try_reconnect_and_query(&addr_key, &axon_info, request, response_timeout)
595 .await
596 }
597 None => {
598 debug!(addr = %addr_key, "query_axon: no connection in registry");
599 self.try_reconnect_and_query(&addr_key, &axon_info, request, response_timeout)
600 .await
601 }
602 }
603 }
604
605 #[instrument(skip(self, axon_info, request), fields(miner_ip = %axon_info.ip, miner_port = axon_info.port, timeout_ms = timeout.as_millis() as u64))]
614 pub async fn query_axon_with_timeout(
615 &self,
616 axon_info: QuicAxonInfo,
617 request: QuicRequest,
618 timeout: Duration,
619 ) -> Result<QuicResponse> {
620 self.query_axon_inner(axon_info, request, Some(timeout))
621 .await
622 }
623
624 #[instrument(skip(self, axon_info, request), fields(miner_ip = %axon_info.ip, miner_port = axon_info.port))]
626 pub async fn query_axon_stream(
627 &self,
628 axon_info: QuicAxonInfo,
629 request: QuicRequest,
630 ) -> Result<StreamingResponse> {
631 let addr_key = axon_info.addr_key();
632
633 let (connection, bound) = {
634 let state = self.state.read().await;
635 (
636 state.registry.get_connection(&addr_key),
637 state
638 .registry
639 .is_authenticated_at(&axon_info.hotkey, &addr_key),
640 )
641 };
642
643 let max_fp = self.config.max_frame_payload_bytes;
644 let max_sp = self.config.max_stream_payload_bytes;
645 match connection {
646 Some(conn) if conn.close_reason().is_none() => {
647 if !bound {
648 return Err(LightningError::Handshake(format!(
649 "no authenticated route for {} at {}",
650 axon_info.hotkey, addr_key
651 )));
652 }
653 open_streaming_synapse(
654 &conn,
655 request,
656 max_fp,
657 max_sp,
658 self.config.stream_chunk_timeout,
659 )
660 .await
661 }
662 Some(conn) => {
663 let reason = conn.close_reason();
664 warn!(
665 addr = %addr_key,
666 close_reason = ?reason,
667 "QUIC connection closed, triggering reconnect (stream)"
668 );
669 self.try_reconnect_and_stream(&addr_key, &axon_info, request)
670 .await
671 }
672 None => {
673 self.try_reconnect_and_stream(&addr_key, &axon_info, request)
674 .await
675 }
676 }
677 }
678
679 async fn try_reconnect_and_query(
680 &self,
681 addr_key: &PeerAddr,
682 axon_info: &QuicAxonInfo,
683 request: QuicRequest,
684 response_timeout: Option<Duration>,
685 ) -> Result<QuicResponse> {
686 let connection = self.try_reconnect(addr_key, axon_info).await?;
687 send_synapse_packet(
688 &connection,
689 request,
690 self.config.max_frame_payload_bytes,
691 response_timeout,
692 )
693 .await
694 }
695
696 async fn try_reconnect_and_stream(
697 &self,
698 addr_key: &PeerAddr,
699 axon_info: &QuicAxonInfo,
700 request: QuicRequest,
701 ) -> Result<StreamingResponse> {
702 let connection = self.try_reconnect(addr_key, axon_info).await?;
703 open_streaming_synapse(
704 &connection,
705 request,
706 self.config.max_frame_payload_bytes,
707 self.config.max_stream_payload_bytes,
708 self.config.stream_chunk_timeout,
709 )
710 .await
711 }
712
713 async fn try_reconnect(
714 &self,
715 addr_key: &PeerAddr,
716 axon_info: &QuicAxonInfo,
717 ) -> Result<Connection> {
718 let endpoint = self
719 .endpoint
720 .as_ref()
721 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
722 .clone();
723 let signer = self
724 .signer
725 .as_ref()
726 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
727 .clone();
728
729 {
730 let mut state = self.state.write().await;
731 if let Err(rejection) = state.registry.try_start_reconnect(
732 addr_key.clone(),
733 self.config.reconnect_max_retries,
734 self.config.reconnect_slow_probe_interval,
735 ) {
736 use crate::registry::ReconnectRejection;
737 return match rejection {
738 ReconnectRejection::Backoff { next } => {
739 Err(LightningError::Connection(format!(
740 "Reconnection to {} in backoff, next retry in {:?}",
741 addr_key,
742 next.saturating_duration_since(Instant::now())
743 )))
744 }
745 ReconnectRejection::Exhausted { attempts } => {
746 Err(LightningError::Connection(format!(
747 "Reconnection attempts exhausted for {} ({}/{}), awaiting registry refresh",
748 addr_key, attempts, self.config.reconnect_max_retries
749 )))
750 }
751 ReconnectRejection::InProgress => {
752 Err(LightningError::Connection(format!(
753 "Reconnection to {} already in progress",
754 addr_key
755 )))
756 }
757 };
758 }
759 }
760
761 warn!("Connection to {} dead, attempting reconnection", addr_key);
762
763 let reconnect_result = tokio::time::timeout(
764 self.config.connect_timeout,
765 connect_and_handshake(
766 endpoint,
767 axon_info.clone(),
768 self.wallet_hotkey.clone(),
769 signer.clone(),
770 self.config.max_frame_payload_bytes,
771 ),
772 )
773 .await;
774
775 let reconnect_result = match reconnect_result {
776 Ok(r) => r,
777 Err(_) => Err(LightningError::Connection(format!(
778 "Reconnection to {} timed out",
779 addr_key
780 ))),
781 };
782
783 match reconnect_result {
784 Ok(connection) => {
785 let co_located: Vec<String> = {
786 let state = self.state.read().await;
787 state
788 .registry
789 .hotkeys_at_addr(addr_key)
790 .into_iter()
791 .filter(|hk| *hk != axon_info.hotkey)
792 .collect()
793 };
794 let mut failed_hotkeys = Vec::new();
795 for hk in &co_located {
796 match tokio::time::timeout(
797 self.config.connect_timeout,
798 authenticate_handshake(
799 &connection,
800 hk,
801 &self.wallet_hotkey,
802 &signer,
803 self.config.max_frame_payload_bytes,
804 ),
805 )
806 .await
807 {
808 Ok(Ok(())) => {
809 info!(
810 "Re-authenticated co-located miner {} on reconnected {}",
811 hk, addr_key
812 );
813 }
814 Ok(Err(e)) => {
815 warn!(
816 "Re-authentication failed for co-located miner {} at {}: {}",
817 hk, addr_key, e
818 );
819 failed_hotkeys.push(hk.clone());
820 }
821 Err(_) => {
822 warn!(
823 "Re-authentication timed out for co-located miner {} at {}",
824 hk, addr_key
825 );
826 failed_hotkeys.push(hk.clone());
827 }
828 }
829 }
830
831 let mut state = self.state.write().await;
832 for hk in &failed_hotkeys {
833 state.registry.deregister(hk);
834 }
835 state.registry.register(axon_info.clone());
836 state
837 .registry
838 .set_connection(addr_key.clone(), connection.clone());
839 state.registry.remove_reconnect_state(addr_key);
840 info!("Reconnected to {}", addr_key);
841 Ok(connection)
842 }
843 Err(e) => {
844 let mut state = self.state.write().await;
845 let rs = state.registry.reconnect_state_or_insert(addr_key.clone());
846 rs.in_progress = false;
847 let shift = rs.attempts.min(20);
848 rs.attempts += 1;
849 let in_slow_probe = rs.attempts >= self.config.reconnect_max_retries;
850 if in_slow_probe {
851 if let Some(probe_interval) = self.config.reconnect_slow_probe_interval {
852 rs.next_retry_at = Instant::now() + probe_interval;
853 warn!(
854 "Slow probe to {} failed, next probe in {:?}: {}",
855 addr_key, probe_interval, e
856 );
857 }
858 } else {
859 let backoff = self
860 .config
861 .reconnect_initial_backoff
862 .checked_mul(2u32.pow(shift))
863 .map(|d| d.min(self.config.reconnect_max_backoff))
864 .unwrap_or(self.config.reconnect_max_backoff);
865 rs.next_retry_at = Instant::now() + backoff;
866 error!(
867 "Reconnection to {} failed (attempt {}/{}), next retry in {:?}: {}",
868 addr_key, rs.attempts, self.config.reconnect_max_retries, backoff, e
869 );
870 }
871 Err(e)
872 }
873 }
874 }
875
876 #[instrument(skip(self, miners), fields(miner_count = miners.len()))]
879 pub async fn update_miner_registry(&self, miners: Vec<QuicAxonInfo>) -> Result<()> {
880 let endpoint = self
881 .endpoint
882 .as_ref()
883 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
884 .clone();
885 let signer = self
886 .signer
887 .as_ref()
888 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
889 .clone();
890 update_miner_registry_inner(
891 &self.state,
892 &endpoint,
893 &self.wallet_hotkey,
894 &signer,
895 &self.config,
896 miners,
897 )
898 .await
899 }
900
901 pub async fn authenticated_hotkeys(&self) -> std::collections::HashSet<String> {
907 let state = self.state.read().await;
908 state.registry.active_hotkeys().into_iter().collect()
909 }
910
911 #[instrument(skip(self))]
913 pub async fn get_connection_stats(&self) -> Result<HashMap<String, String>> {
914 let state = self.state.read().await;
915
916 let mut stats = HashMap::new();
917 stats.insert(
918 "total_connections".to_string(),
919 state.registry.connection_count().to_string(),
920 );
921 stats.insert(
922 "active_miners".to_string(),
923 state.registry.active_miner_count().to_string(),
924 );
925
926 for addr_key in state.registry.connection_addrs() {
927 let status = match state.registry.get_connection(addr_key) {
928 Some(conn) => {
929 if let Some(reason) = conn.close_reason() {
930 format!("closed({:?})", reason)
931 } else {
932 "active".to_string()
933 }
934 }
935 None => "missing".to_string(),
936 };
937 stats.insert(format!("connection_{}", addr_key), status);
938 }
939
940 Ok(stats)
941 }
942
943 #[cfg(feature = "subtensor")]
946 pub async fn start_metagraph_monitor(
947 &self,
948 monitor_config: MetagraphMonitorConfig,
949 ) -> Result<()> {
950 if monitor_config.sync_interval.is_zero() {
951 return Err(LightningError::Config(
952 "sync_interval must be non-zero".into(),
953 ));
954 }
955
956 self.stop_metagraph_monitor().await;
957
958 let endpoint = self
959 .endpoint
960 .as_ref()
961 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
962 .clone();
963 let signer = self
964 .signer
965 .as_ref()
966 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
967 .clone();
968
969 let subtensor = tokio::time::timeout(
970 Duration::from_secs(30),
971 connect_subtensor::<PolkadotConfig>(&monitor_config.subtensor_endpoint),
972 )
973 .await
974 .map_err(|_| LightningError::Handler("subtensor connection timed out after 30s".into()))?
975 .map_err(|e| LightningError::Handler(format!("connecting to subtensor: {}", e)))?;
976
977 let mut metagraph = Metagraph::new(monitor_config.netuid);
978 tokio::time::timeout(Duration::from_secs(60), metagraph.sync(&subtensor))
979 .await
980 .map_err(|_| {
981 LightningError::Handler("initial metagraph sync timed out after 60s".into())
982 })??;
983
984 let miners = metagraph.quic_miners();
985 info!(
986 netuid = monitor_config.netuid,
987 miners = miners.len(),
988 "initial metagraph sync complete"
989 );
990
991 update_miner_registry_inner(
992 &self.state,
993 &endpoint,
994 &self.wallet_hotkey,
995 &signer,
996 &self.config,
997 miners,
998 )
999 .await?;
1000
1001 let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
1002 let state = self.state.clone();
1003 let wallet_hotkey = self.wallet_hotkey.clone();
1004 let config = self.config.clone();
1005 let sync_interval = monitor_config.sync_interval;
1006 let subtensor_url = monitor_config.subtensor_endpoint.clone();
1007
1008 let handle = tokio::spawn(async move {
1009 let mut interval = tokio::time::interval(sync_interval);
1010 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1011 interval.tick().await;
1012 let mut subtensor = subtensor;
1013
1014 loop {
1015 tokio::select! {
1016 _ = interval.tick() => {}
1017 _ = shutdown_rx.changed() => {
1018 info!("metagraph monitor shutting down");
1019 return;
1020 }
1021 }
1022
1023 let sync_result =
1024 tokio::time::timeout(Duration::from_secs(60), metagraph.sync(&subtensor)).await;
1025
1026 let needs_reconnect = match sync_result {
1027 Ok(Ok(())) => {
1028 let miners = metagraph.quic_miners();
1029 info!(
1030 netuid = metagraph.netuid,
1031 miners = miners.len(),
1032 block = metagraph.block,
1033 "metagraph resync complete"
1034 );
1035 if let Err(e) = update_miner_registry_inner(
1036 &state,
1037 &endpoint,
1038 &wallet_hotkey,
1039 &signer,
1040 &config,
1041 miners,
1042 )
1043 .await
1044 {
1045 error!("registry update after metagraph sync failed: {}", e);
1046 }
1047 false
1048 }
1049 Ok(Err(e)) => {
1050 error!("metagraph sync failed, reconnecting to subtensor: {}", e);
1051 true
1052 }
1053 Err(_) => {
1054 error!("metagraph sync timed out after 60s, reconnecting to subtensor");
1055 true
1056 }
1057 };
1058
1059 if needs_reconnect {
1060 match tokio::time::timeout(
1061 Duration::from_secs(30),
1062 connect_subtensor::<PolkadotConfig>(&subtensor_url),
1063 )
1064 .await
1065 {
1066 Ok(Ok(new_client)) => {
1067 subtensor = new_client;
1068 info!("subtensor client reconnected");
1069 }
1070 Ok(Err(e)) => {
1071 error!("subtensor reconnection failed: {}", e);
1072 }
1073 Err(_) => {
1074 error!("subtensor reconnection timed out after 30s");
1075 }
1076 }
1077 }
1078 }
1079 });
1080
1081 let mut st = self.state.write().await;
1082 st.metagraph_shutdown = Some(shutdown_tx);
1083 st.metagraph_handle = Some(handle);
1084 Ok(())
1085 }
1086
1087 #[cfg(feature = "subtensor")]
1089 pub async fn stop_metagraph_monitor(&self) {
1090 let (shutdown_tx, handle) = {
1091 let mut st = self.state.write().await;
1092 (st.metagraph_shutdown.take(), st.metagraph_handle.take())
1093 };
1094 if let Some(tx) = shutdown_tx {
1095 let _ = tx.send(true);
1096 }
1097 if let Some(mut handle) = handle {
1098 if tokio::time::timeout(Duration::from_secs(5), &mut handle)
1099 .await
1100 .is_err()
1101 {
1102 warn!("metagraph monitor did not shut down within 5s, aborting");
1103 handle.abort();
1104 let _ = handle.await;
1105 }
1106 }
1107 }
1108
1109 #[instrument(skip(self))]
1111 pub async fn close_all_connections(&self) -> Result<()> {
1112 #[cfg(feature = "subtensor")]
1113 self.stop_metagraph_monitor().await;
1114
1115 let mut state = self.state.write().await;
1116
1117 for (_, connection) in state.registry.drain_connections() {
1118 connection.close(0u32.into(), b"client_shutdown");
1119 }
1120
1121 state.registry.clear();
1122
1123 info!("All Lightning QUIC connections closed");
1124 Ok(())
1125 }
1126}
1127
1128async fn update_miner_registry_inner(
1129 state: &Arc<RwLock<ClientState>>,
1130 endpoint: &Endpoint,
1131 wallet_hotkey: &str,
1132 signer: &Arc<dyn Signer>,
1133 config: &LightningClientConfig,
1134 miners: Vec<QuicAxonInfo>,
1135) -> Result<()> {
1136 let new_by_hotkey: HashMap<String, QuicAxonInfo> = miners
1137 .iter()
1138 .map(|m| (m.hotkey.clone(), m.clone()))
1139 .collect();
1140
1141 let new_hotkeys_needing_auth: Vec<QuicAxonInfo>;
1142 let new_addrs_needing_connect: HashMap<PeerAddr, Vec<QuicAxonInfo>>;
1143 {
1144 let mut st = state.write().await;
1145
1146 let active_hotkeys = st.registry.active_hotkeys();
1147 for hotkey in active_hotkeys {
1148 if !new_by_hotkey.contains_key(&hotkey) {
1149 if let Some(miner) = st.registry.deregister(&hotkey) {
1150 let addr_key = miner.addr_key();
1151 info!("Miner {} deregistered from {}", hotkey, addr_key);
1152 if !st.registry.addr_has_hotkeys(&addr_key) {
1153 if let Some(connection) = st.registry.remove_connection(&addr_key) {
1154 connection.close(0u32.into(), b"miner_deregistered");
1155 }
1156 st.registry.remove_reconnect_state(&addr_key);
1157 }
1158 }
1159 }
1160 }
1161
1162 let active_addrs = st.registry.active_addrs();
1163 for addr_key in &active_addrs {
1164 if st.registry.remove_reconnect_state(addr_key) {
1165 info!(
1166 "Registry refresh reset reconnection backoff for {}",
1167 addr_key
1168 );
1169 }
1170 }
1171
1172 let dead_addrs: Vec<PeerAddr> = active_addrs
1173 .iter()
1174 .filter(|addr| {
1175 st.registry
1176 .get_connection(addr)
1177 .is_some_and(|c| c.close_reason().is_some())
1178 })
1179 .cloned()
1180 .collect();
1181 for addr_key in &dead_addrs {
1182 if let Some(conn) = st.registry.remove_connection(addr_key) {
1183 let hotkeys = st.registry.hotkeys_at_addr(addr_key);
1184 info!(
1185 addr = %addr_key,
1186 close_reason = ?conn.close_reason(),
1187 hotkeys = ?hotkeys,
1188 "Pruning dead connection and deregistering miners"
1189 );
1190 for hk in &hotkeys {
1191 st.registry.deregister(hk);
1192 }
1193 }
1194 }
1195
1196 for new_miner in new_by_hotkey.values() {
1197 if let Some(old_miner) = st.registry.active_miner(&new_miner.hotkey) {
1198 let old_addr = old_miner.addr_key();
1199 let new_addr = new_miner.addr_key();
1200 if old_addr != new_addr {
1201 info!(
1202 "Miner {} changed address from {} to {}",
1203 new_miner.hotkey, old_addr, new_addr
1204 );
1205 st.registry.deregister(&new_miner.hotkey);
1206 if !st.registry.addr_has_hotkeys(&old_addr) {
1207 if let Some(conn) = st.registry.remove_connection(&old_addr) {
1208 conn.close(0u32.into(), b"miner_addr_changed");
1209 }
1210 st.registry.remove_reconnect_state(&old_addr);
1211 }
1212 }
1213 }
1214 }
1215
1216 let new_hotkeys: Vec<QuicAxonInfo> = new_by_hotkey
1217 .values()
1218 .filter(|m| !st.registry.contains_active_miner(&m.hotkey))
1219 .cloned()
1220 .collect();
1221
1222 let mut need_auth = Vec::new();
1223 let mut need_connect: HashMap<PeerAddr, Vec<QuicAxonInfo>> = HashMap::new();
1224 for miner in new_hotkeys {
1225 let addr_key = miner.addr_key();
1226 if st.registry.contains_connection(&addr_key) {
1227 need_auth.push(miner);
1228 } else {
1229 need_connect.entry(addr_key).or_default().push(miner);
1230 }
1231 }
1232
1233 let active_count = st.registry.connection_count();
1234 let remaining_capacity = config.max_connections.saturating_sub(active_count);
1235 if need_connect.len() > remaining_capacity {
1236 warn!(
1237 "Connection limit ({}) reached with {} active, skipping {} of {} new addresses",
1238 config.max_connections,
1239 active_count,
1240 need_connect.len() - remaining_capacity,
1241 need_connect.len()
1242 );
1243 }
1244
1245 new_hotkeys_needing_auth = need_auth;
1246 new_addrs_needing_connect = need_connect.into_iter().take(remaining_capacity).collect();
1247 }
1248
1249 let timeout = config.connect_timeout;
1250 let max_fp = config.max_frame_payload_bytes;
1251
1252 if !new_hotkeys_needing_auth.is_empty() {
1253 let miners_with_conns: Vec<(QuicAxonInfo, Connection)> = {
1254 let st = state.read().await;
1255 new_hotkeys_needing_auth
1256 .into_iter()
1257 .filter_map(|miner| {
1258 let addr_key = miner.addr_key();
1259 st.registry
1260 .get_connection(&addr_key)
1261 .map(|conn| (miner, conn))
1262 })
1263 .collect()
1264 };
1265
1266 let mut authenticated = Vec::new();
1267 for (miner, conn) in &miners_with_conns {
1268 let addr_key = miner.addr_key();
1269 match tokio::time::timeout(
1270 timeout,
1271 authenticate_handshake(conn, &miner.hotkey, wallet_hotkey, signer, max_fp),
1272 )
1273 .await
1274 {
1275 Ok(Ok(())) => {
1276 info!(
1277 "Authenticated new miner {} on existing connection to {}",
1278 miner.hotkey, addr_key
1279 );
1280 authenticated.push(miner.clone());
1281 }
1282 Ok(Err(e)) => {
1283 warn!(
1284 "Handshake failed for new hotkey {} at {}: {}",
1285 miner.hotkey, addr_key, e
1286 );
1287 }
1288 Err(_) => {
1289 warn!(
1290 "Handshake timed out for new hotkey {} at {}",
1291 miner.hotkey, addr_key
1292 );
1293 }
1294 }
1295 }
1296
1297 let mut st = state.write().await;
1298 for miner in authenticated {
1299 st.registry.register(miner);
1300 }
1301 }
1302
1303 if !new_addrs_needing_connect.is_empty() {
1304 let mut set = tokio::task::JoinSet::new();
1305 for (addr_key, miners_at_addr) in new_addrs_needing_connect {
1306 info!(
1307 "New address detected, establishing QUIC connection: {}",
1308 addr_key
1309 );
1310 let ep = endpoint.clone();
1311 let wh = wallet_hotkey.to_string();
1312 let s = signer.clone();
1313 set.spawn(connect_and_authenticate_per_address(
1314 ep,
1315 wh,
1316 s,
1317 addr_key,
1318 miners_at_addr,
1319 timeout,
1320 max_fp,
1321 ));
1322 }
1323
1324 let mut results = Vec::new();
1325 while let Some(join_result) = set.join_next().await {
1326 match join_result {
1327 Ok((addr_key, conn_result, authenticated)) => {
1328 results.push((addr_key, conn_result, authenticated));
1329 }
1330 Err(e) => {
1331 error!("Connection task panicked: {}", e);
1332 }
1333 }
1334 }
1335
1336 let mut st = state.write().await;
1337 for (addr_key, conn_result, authenticated) in results {
1338 match conn_result {
1339 Ok(connection) => {
1340 if authenticated.is_empty() {
1341 warn!(
1342 "No hotkeys authenticated at {}, dropping connection",
1343 addr_key
1344 );
1345 connection.close(0u32.into(), b"no_authenticated_hotkeys");
1346 } else {
1347 for miner in authenticated {
1348 st.registry.register(miner);
1349 }
1350 st.registry.set_connection(addr_key, connection);
1351 }
1352 }
1353 Err(e) => {
1354 error!("Failed to connect to {}: {}", addr_key, e);
1355 }
1356 }
1357 }
1358 }
1359
1360 Ok(())
1361}
1362
1363fn get_peer_cert_fingerprint(connection: &Connection) -> Option<[u8; 32]> {
1364 let identity = connection.peer_identity()?;
1365 let certs = identity.downcast::<Vec<CertificateDer<'static>>>().ok()?;
1366 let first = certs.first()?;
1367 Some(blake2_256(first.as_ref()))
1368}
1369
1370async fn quic_connect(
1371 endpoint: &Endpoint,
1372 addr_key: &PeerAddr,
1373 server_name: &str,
1374) -> Result<Connection> {
1375 let addr: SocketAddr = addr_key
1376 .as_ref()
1377 .parse()
1378 .map_err(|e| LightningError::Connection(format!("Invalid address: {}", e)))?;
1379
1380 endpoint
1381 .connect(addr, server_name)
1382 .map_err(|e| LightningError::Connection(format!("Connection failed: {}", e)))?
1383 .await
1384 .map_err(|e| LightningError::Connection(format!("Connection handshake failed: {}", e)))
1385}
1386
1387async fn connect_and_authenticate_per_address(
1388 endpoint: Endpoint,
1389 wallet_hotkey: String,
1390 signer: Arc<dyn Signer>,
1391 addr_key: PeerAddr,
1392 miners_at_addr: Vec<QuicAxonInfo>,
1393 timeout: Duration,
1394 max_frame_payload: usize,
1395) -> (PeerAddr, Result<Connection>, Vec<QuicAxonInfo>) {
1396 let first = match miners_at_addr.first() {
1397 Some(m) => m,
1398 None => {
1399 return (
1400 addr_key,
1401 Err(LightningError::Connection("no miners for address".into())),
1402 vec![],
1403 );
1404 }
1405 };
1406
1407 let conn = match tokio::time::timeout(timeout, quic_connect(&endpoint, &addr_key, &first.ip))
1408 .await
1409 {
1410 Ok(Ok(c)) => c,
1411 Ok(Err(e)) => return (addr_key, Err(e), vec![]),
1412 Err(_) => {
1413 let err = LightningError::Connection(format!("Connection to {} timed out", addr_key));
1414 return (addr_key, Err(err), vec![]);
1415 }
1416 };
1417
1418 let mut authenticated = Vec::new();
1419 for miner in &miners_at_addr {
1420 match tokio::time::timeout(
1421 timeout,
1422 authenticate_handshake(
1423 &conn,
1424 &miner.hotkey,
1425 &wallet_hotkey,
1426 &signer,
1427 max_frame_payload,
1428 ),
1429 )
1430 .await
1431 {
1432 Ok(Ok(())) => authenticated.push(miner.clone()),
1433 Ok(Err(e)) => {
1434 warn!(
1435 "Handshake failed for hotkey {} at {}: {}",
1436 miner.hotkey, addr_key, e
1437 );
1438 }
1439 Err(_) => {
1440 warn!(
1441 "Handshake timed out for hotkey {} at {}",
1442 miner.hotkey, addr_key
1443 );
1444 }
1445 }
1446 }
1447
1448 (addr_key, Ok(conn), authenticated)
1449}
1450
1451async fn authenticate_handshake(
1452 connection: &Connection,
1453 expected_hotkey: &str,
1454 wallet_hotkey: &str,
1455 signer: &Arc<dyn Signer>,
1456 max_frame_payload: usize,
1457) -> Result<()> {
1458 let peer_cert_fp = get_peer_cert_fingerprint(connection).ok_or_else(|| {
1459 LightningError::Handshake("peer certificate not available for fingerprinting".to_string())
1460 })?;
1461 let peer_cert_fp_b64 = BASE64_STANDARD.encode(peer_cert_fp);
1462
1463 let nonce = generate_nonce();
1464 let timestamp = unix_timestamp_secs();
1465 let message = handshake_request_message(wallet_hotkey, timestamp, &nonce, &peer_cert_fp_b64);
1466 let msg_bytes = message.into_bytes();
1467 let signer_clone = signer.clone();
1468 let signature_bytes = tokio::task::spawn_blocking(move || signer_clone.sign(&msg_bytes))
1469 .await
1470 .map_err(|e| LightningError::Signing(format!("signer task failed: {}", e)))??;
1471
1472 let handshake_request = HandshakeRequest {
1473 validator_hotkey: wallet_hotkey.to_string(),
1474 timestamp,
1475 nonce: nonce.clone(),
1476 signature: BASE64_STANDARD.encode(&signature_bytes),
1477 };
1478
1479 let response = send_handshake(connection, handshake_request, max_frame_payload).await?;
1480 if !response.accepted {
1481 return Err(LightningError::Handshake(
1482 "Handshake rejected by miner".into(),
1483 ));
1484 }
1485
1486 if response.miner_hotkey != expected_hotkey {
1487 return Err(LightningError::Handshake(format!(
1488 "Miner hotkey mismatch: expected {}, got {}",
1489 expected_hotkey, response.miner_hotkey
1490 )));
1491 }
1492
1493 match response.cert_fingerprint {
1494 Some(ref resp_fp) if *resp_fp == peer_cert_fp_b64 => {}
1495 Some(_) => {
1496 return Err(LightningError::Handshake(
1497 "Cert fingerprint mismatch between TLS session and handshake response".to_string(),
1498 ));
1499 }
1500 None => {
1501 return Err(LightningError::Handshake(
1502 "Miner handshake response omitted required cert fingerprint".to_string(),
1503 ));
1504 }
1505 }
1506
1507 verify_miner_response_signature(&response, wallet_hotkey, &nonce, &peer_cert_fp_b64).await?;
1508
1509 info!("Handshake successful with miner {}", expected_hotkey);
1510 Ok(())
1511}
1512
1513async fn connect_and_handshake(
1514 endpoint: Endpoint,
1515 miner: QuicAxonInfo,
1516 wallet_hotkey: String,
1517 signer: Arc<dyn Signer>,
1518 max_frame_payload: usize,
1519) -> Result<Connection> {
1520 let addr_key = miner.addr_key();
1521 let connection = quic_connect(&endpoint, &addr_key, &miner.ip).await?;
1522 authenticate_handshake(
1523 &connection,
1524 &miner.hotkey,
1525 &wallet_hotkey,
1526 &signer,
1527 max_frame_payload,
1528 )
1529 .await?;
1530 Ok(connection)
1531}
1532
1533async fn verify_miner_response_signature(
1534 response: &HandshakeResponse,
1535 validator_hotkey: &str,
1536 nonce: &str,
1537 cert_fp_b64: &str,
1538) -> Result<()> {
1539 if response.signature.is_empty() {
1540 return Err(LightningError::Handshake(
1541 "Miner returned empty signature".to_string(),
1542 ));
1543 }
1544
1545 let expected_message = handshake_response_message(
1546 validator_hotkey,
1547 &response.miner_hotkey,
1548 response.timestamp,
1549 nonce,
1550 cert_fp_b64,
1551 );
1552
1553 let valid = crate::signing::verify_sr25519_signature(
1554 &response.miner_hotkey,
1555 &response.signature,
1556 &expected_message,
1557 )
1558 .await?;
1559
1560 if !valid {
1561 return Err(LightningError::Handshake(
1562 "Miner response signature verification failed".to_string(),
1563 ));
1564 }
1565
1566 Ok(())
1567}
1568
1569async fn send_handshake(
1570 connection: &Connection,
1571 request: HandshakeRequest,
1572 max_frame_payload: usize,
1573) -> Result<HandshakeResponse> {
1574 let (mut send, mut recv) = connection.open_bi().await.map_err(|e| {
1575 LightningError::Connection(format!("Failed to open bidirectional stream: {}", e))
1576 })?;
1577
1578 let request_bytes = rmp_serde::to_vec(&request).map_err(|e| {
1579 LightningError::Serialization(format!("Failed to serialize handshake: {}", e))
1580 })?;
1581
1582 write_frame_and_finish(&mut send, MessageType::HandshakeRequest, &request_bytes).await?;
1583
1584 let (msg_type, payload) = read_frame(&mut recv, max_frame_payload).await?;
1585 if msg_type != MessageType::HandshakeResponse {
1586 return Err(LightningError::Handshake(format!(
1587 "Expected HandshakeResponse, got {:?}",
1588 msg_type
1589 )));
1590 }
1591
1592 let response: HandshakeResponse = rmp_serde::from_slice(&payload).map_err(|e| {
1593 LightningError::Serialization(format!("Failed to parse handshake response: {}", e))
1594 })?;
1595
1596 Ok(response)
1597}
1598
1599const WRITE_BUDGET_FLOOR_SECS: u64 = 5;
1602const WRITE_BUDGET_MIN_BYTES_PER_SEC: u64 = 1024 * 1024;
1606const WRITE_BUDGET_MAX_SECS: u64 = 60;
1609
1610fn write_budget_for(len: usize) -> Duration {
1615 let transfer_secs = (len as u64).div_ceil(WRITE_BUDGET_MIN_BYTES_PER_SEC);
1616 Duration::from_secs((WRITE_BUDGET_FLOOR_SECS + transfer_secs).min(WRITE_BUDGET_MAX_SECS))
1617}
1618
1619async fn send_synapse_frame(send: &mut quinn::SendStream, request: QuicRequest) -> Result<()> {
1620 let synapse_packet = SynapsePacket {
1621 synapse_type: request.synapse_type,
1622 data: request.data,
1623 timestamp: unix_timestamp_secs(),
1624 };
1625
1626 let packet_bytes = rmp_serde::to_vec(&synapse_packet).map_err(|e| {
1627 LightningError::Serialization(format!("Failed to serialize synapse packet: {}", e))
1628 })?;
1629
1630 let budget = write_budget_for(packet_bytes.len());
1631 tokio::time::timeout(
1632 budget,
1633 write_frame_and_finish(send, MessageType::SynapsePacket, &packet_bytes),
1634 )
1635 .await
1636 .map_err(|_| {
1637 LightningError::Transport(format!(
1638 "request transfer timed out after {:?} ({} bytes unacknowledged by peer)",
1639 budget,
1640 packet_bytes.len()
1641 ))
1642 })?
1643}
1644
1645async fn send_synapse_packet(
1646 connection: &Connection,
1647 request: QuicRequest,
1648 max_frame_payload: usize,
1649 response_timeout: Option<Duration>,
1650) -> Result<QuicResponse> {
1651 let stable_id = connection.stable_id();
1652 debug!(stable_id, "send_synapse_packet: opening bi stream");
1653 let (mut send, mut recv) = connection
1654 .open_bi()
1655 .await
1656 .map_err(|e| LightningError::Connection(format!("Failed to open stream: {}", e)))?;
1657 debug!(stable_id, "send_synapse_packet: bi stream opened");
1658
1659 let start = Instant::now();
1660
1661 send_synapse_frame(&mut send, request).await?;
1662 debug!(
1663 stable_id,
1664 "send_synapse_packet: frame sent, awaiting response"
1665 );
1666
1667 let read = read_frame(&mut recv, max_frame_payload);
1668 let (msg_type, payload) = match response_timeout {
1669 Some(t) => tokio::time::timeout(t, read)
1670 .await
1671 .map_err(|_| LightningError::Transport("query timed out".into()))??,
1672 None => read.await?,
1673 };
1674 debug!(stable_id, msg_type = ?msg_type, elapsed_ms = start.elapsed().as_millis() as u64, "send_synapse_packet: response received");
1675
1676 match msg_type {
1677 MessageType::SynapseResponse => {
1678 let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
1679 let synapse_response: SynapseResponse =
1680 rmp_serde::from_slice(&payload).map_err(|e| {
1681 LightningError::Serialization(format!(
1682 "Failed to parse synapse response: {}",
1683 e
1684 ))
1685 })?;
1686
1687 Ok(QuicResponse {
1688 success: synapse_response.success,
1689 data: synapse_response.data,
1690 latency_ms,
1691 error: synapse_response.error,
1692 })
1693 }
1694 MessageType::StreamChunk => Err(LightningError::Transport(
1695 "received StreamChunk on non-streaming query; use query_axon_stream for streaming synapses".to_string(),
1696 )),
1697 other => Err(LightningError::Transport(format!(
1698 "unexpected response type: {:?}",
1699 other
1700 ))),
1701 }
1702}
1703
1704async fn open_streaming_synapse(
1705 connection: &Connection,
1706 request: QuicRequest,
1707 max_frame_payload: usize,
1708 max_stream_payload: usize,
1709 chunk_timeout: Option<Duration>,
1710) -> Result<StreamingResponse> {
1711 let (mut send, recv) = connection
1712 .open_bi()
1713 .await
1714 .map_err(|e| LightningError::Connection(format!("Failed to open stream: {}", e)))?;
1715
1716 send_synapse_frame(&mut send, request).await?;
1717
1718 Ok(StreamingResponse {
1719 recv,
1720 max_payload: max_frame_payload,
1721 max_stream_payload,
1722 chunk_timeout,
1723 })
1724}
1725
1726fn generate_nonce() -> String {
1727 use rand::Rng;
1728 let bytes: [u8; 16] = rand::thread_rng().gen();
1729 format!("{:032x}", u128::from_be_bytes(bytes))
1730}
1731
1732#[cfg(test)]
1733mod tests {
1734 use super::*;
1735 use sp_core::{crypto::Ss58Codec, sr25519, Pair};
1736
1737 const MINER_SEED: [u8; 32] = [1u8; 32];
1738 const VALIDATOR_SEED: [u8; 32] = [2u8; 32];
1739
1740 #[test]
1741 fn write_budget_scales_with_payload_size() {
1742 let floor = write_budget_for(0);
1743 assert_eq!(floor, Duration::from_secs(WRITE_BUDGET_FLOOR_SECS));
1744
1745 let one_mib = write_budget_for(1024 * 1024);
1746 assert_eq!(one_mib, Duration::from_secs(WRITE_BUDGET_FLOOR_SECS + 1));
1747
1748 let ten_mib = write_budget_for(10 * 1024 * 1024);
1749 assert_eq!(ten_mib, Duration::from_secs(WRITE_BUDGET_FLOOR_SECS + 10));
1750 }
1751
1752 #[test]
1753 fn write_budget_is_capped() {
1754 let huge = write_budget_for(usize::MAX);
1755 assert_eq!(huge, Duration::from_secs(WRITE_BUDGET_MAX_SECS));
1756
1757 let at_cap = write_budget_for((WRITE_BUDGET_MAX_SECS as usize) * 1024 * 1024 * 2);
1758 assert_eq!(at_cap, Duration::from_secs(WRITE_BUDGET_MAX_SECS));
1759 }
1760
1761 #[test]
1762 fn write_budget_exceeds_typical_response_timeouts_for_large_payloads() {
1763 let five_mib = write_budget_for(5 * 1024 * 1024);
1768 assert!(five_mib >= Duration::from_secs(10));
1769 }
1770
1771 fn make_signed_response(
1772 miner_seed: [u8; 32],
1773 validator_hotkey: &str,
1774 nonce: &str,
1775 cert_fp_b64: &str,
1776 ) -> HandshakeResponse {
1777 let pair = sr25519::Pair::from_seed(&miner_seed);
1778 let miner_hotkey = pair.public().to_ss58check();
1779 let timestamp = unix_timestamp_secs();
1780 let message = handshake_response_message(
1781 validator_hotkey,
1782 &miner_hotkey,
1783 timestamp,
1784 nonce,
1785 cert_fp_b64,
1786 );
1787 let signature = pair.sign(message.as_bytes());
1788 HandshakeResponse {
1789 miner_hotkey,
1790 timestamp,
1791 signature: BASE64_STANDARD.encode(signature.0),
1792 accepted: true,
1793 connection_id: "test".to_string(),
1794 cert_fingerprint: Some(cert_fp_b64.to_string()),
1795 }
1796 }
1797
1798 fn validator_hotkey() -> String {
1799 sr25519::Pair::from_seed(&VALIDATOR_SEED)
1800 .public()
1801 .to_ss58check()
1802 }
1803
1804 #[tokio::test]
1805 async fn verify_valid_miner_signature() {
1806 let nonce = "test-nonce";
1807 let fp = "dGVzdC1mcA==";
1808 let resp = make_signed_response(MINER_SEED, &validator_hotkey(), nonce, fp);
1809 assert!(
1810 verify_miner_response_signature(&resp, &validator_hotkey(), nonce, fp)
1811 .await
1812 .is_ok()
1813 );
1814 }
1815
1816 #[tokio::test]
1817 async fn verify_rejects_empty_signature() {
1818 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1819 resp.signature = String::new();
1820 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1821 .await
1822 .unwrap_err();
1823 assert!(err.to_string().contains("empty signature"));
1824 }
1825
1826 #[tokio::test]
1827 async fn verify_rejects_invalid_base64() {
1828 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1829 resp.signature = "not-valid-base64!!!".to_string();
1830 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1831 .await
1832 .unwrap_err();
1833 assert!(err.to_string().contains("Failed to decode signature"));
1834 }
1835
1836 #[tokio::test]
1837 async fn verify_rejects_wrong_signature_length() {
1838 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1839 resp.signature = BASE64_STANDARD.encode([0u8; 32]);
1840 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1841 .await
1842 .unwrap_err();
1843 assert!(err.to_string().contains("Invalid signature length"));
1844 }
1845
1846 #[tokio::test]
1847 async fn verify_rejects_bad_ss58_address() {
1848 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1849 resp.miner_hotkey = "not_a_valid_ss58".to_string();
1850 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1851 .await
1852 .unwrap_err();
1853 assert!(err.to_string().contains("Invalid SS58 address"));
1854 }
1855
1856 #[tokio::test]
1857 async fn verify_rejects_wrong_signer() {
1858 let nonce = "n";
1859 let fp = "fp";
1860 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), nonce, fp);
1861 let wrong_pair = sr25519::Pair::from_seed(&[99u8; 32]);
1862 resp.miner_hotkey = wrong_pair.public().to_ss58check();
1863 let err = verify_miner_response_signature(&resp, &validator_hotkey(), nonce, fp)
1864 .await
1865 .unwrap_err();
1866 assert!(err.to_string().contains("signature verification failed"));
1867 }
1868
1869 #[tokio::test]
1870 async fn verify_rejects_tampered_nonce() {
1871 let fp = "fp";
1872 let resp = make_signed_response(MINER_SEED, &validator_hotkey(), "original-nonce", fp);
1873 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "tampered-nonce", fp)
1874 .await
1875 .unwrap_err();
1876 assert!(err.to_string().contains("signature verification failed"));
1877 }
1878
1879 #[test]
1880 fn with_config_rejects_frame_payload_below_minimum() {
1881 let cfg = LightningClientConfig {
1882 max_frame_payload_bytes: 512,
1883 ..LightningClientConfig::default()
1884 };
1885 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1886 }
1887
1888 #[test]
1889 fn with_config_rejects_frame_payload_above_u32_max() {
1890 let too_big: u128 = u32::MAX as u128 + 1;
1891 let val = match usize::try_from(too_big) {
1892 Ok(v) => v,
1893 Err(_) => return,
1894 };
1895 let cfg = LightningClientConfig {
1896 max_frame_payload_bytes: val,
1897 max_stream_payload_bytes: val,
1898 ..LightningClientConfig::default()
1899 };
1900 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1901 }
1902
1903 #[test]
1904 fn with_config_rejects_stream_below_frame() {
1905 let base = LightningClientConfig::default();
1906 let cfg = LightningClientConfig {
1907 max_stream_payload_bytes: base.max_frame_payload_bytes - 1,
1908 ..base
1909 };
1910 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1911 }
1912
1913 #[test]
1914 fn with_config_rejects_zero_stream_chunk_timeout() {
1915 let cfg = LightningClientConfig {
1916 stream_chunk_timeout: Some(Duration::ZERO),
1917 ..LightningClientConfig::default()
1918 };
1919 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1920 }
1921
1922 #[test]
1923 fn with_config_default_succeeds() {
1924 assert!(
1925 LightningClient::with_config("hk".into(), LightningClientConfig::default()).is_ok()
1926 );
1927 }
1928}
1929
1930#[derive(Debug)]
1935struct AcceptAnyCertVerifier;
1936
1937impl ServerCertVerifier for AcceptAnyCertVerifier {
1938 fn verify_server_cert(
1939 &self,
1940 _end_entity: &CertificateDer<'_>,
1941 _intermediates: &[CertificateDer<'_>],
1942 _server_name: &ServerName<'_>,
1943 _ocsp_response: &[u8],
1944 _now: UnixTime,
1945 ) -> std::result::Result<ServerCertVerified, rustls::Error> {
1946 Ok(ServerCertVerified::assertion())
1947 }
1948
1949 fn verify_tls12_signature(
1950 &self,
1951 _message: &[u8],
1952 _cert: &CertificateDer<'_>,
1953 _dss: &rustls::DigitallySignedStruct,
1954 ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
1955 Err(rustls::Error::PeerIncompatible(
1956 rustls::PeerIncompatible::Tls12NotOffered,
1957 ))
1958 }
1959
1960 fn verify_tls13_signature(
1961 &self,
1962 _message: &[u8],
1963 _cert: &CertificateDer<'_>,
1964 _dss: &rustls::DigitallySignedStruct,
1965 ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
1966 Ok(HandshakeSignatureValid::assertion())
1967 }
1968
1969 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1970 rustls::crypto::ring::default_provider()
1971 .signature_verification_algorithms
1972 .supported_schemes()
1973 }
1974}