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::{Metagraph, MetagraphMonitorConfig};
27#[cfg(feature = "subtensor")]
28use subxt::{OnlineClient, 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 let addr_key = axon_info.addr_key();
550
551 let (connection, bound) = {
552 let state = self.state.read().await;
553 (
554 state.registry.get_connection(&addr_key),
555 state
556 .registry
557 .is_authenticated_at(&axon_info.hotkey, &addr_key),
558 )
559 };
560
561 let max_fp = self.config.max_frame_payload_bytes;
562 match connection {
563 Some(conn) if conn.close_reason().is_none() => {
564 if !bound {
565 return Err(LightningError::Handshake(format!(
566 "no authenticated route for {} at {}",
567 axon_info.hotkey, addr_key
568 )));
569 }
570 debug!(
571 addr = %addr_key,
572 stable_id = conn.stable_id(),
573 "query_axon: connection alive, sending synapse"
574 );
575 send_synapse_packet(&conn, request, max_fp).await
576 }
577 Some(conn) => {
578 let reason = conn.close_reason();
579 warn!(
580 addr = %addr_key,
581 stable_id = conn.stable_id(),
582 close_reason = ?reason,
583 "QUIC connection closed, triggering reconnect"
584 );
585 self.try_reconnect_and_query(&addr_key, &axon_info, request)
586 .await
587 }
588 None => {
589 debug!(addr = %addr_key, "query_axon: no connection in registry");
590 self.try_reconnect_and_query(&addr_key, &axon_info, request)
591 .await
592 }
593 }
594 }
595
596 #[instrument(skip(self, axon_info, request), fields(miner_ip = %axon_info.ip, miner_port = axon_info.port, timeout_ms = timeout.as_millis() as u64))]
598 pub async fn query_axon_with_timeout(
599 &self,
600 axon_info: QuicAxonInfo,
601 request: QuicRequest,
602 timeout: Duration,
603 ) -> Result<QuicResponse> {
604 tokio::time::timeout(timeout, self.query_axon(axon_info, request))
605 .await
606 .map_err(|_| LightningError::Transport("query timed out".into()))?
607 }
608
609 #[instrument(skip(self, axon_info, request), fields(miner_ip = %axon_info.ip, miner_port = axon_info.port))]
611 pub async fn query_axon_stream(
612 &self,
613 axon_info: QuicAxonInfo,
614 request: QuicRequest,
615 ) -> Result<StreamingResponse> {
616 let addr_key = axon_info.addr_key();
617
618 let (connection, bound) = {
619 let state = self.state.read().await;
620 (
621 state.registry.get_connection(&addr_key),
622 state
623 .registry
624 .is_authenticated_at(&axon_info.hotkey, &addr_key),
625 )
626 };
627
628 let max_fp = self.config.max_frame_payload_bytes;
629 let max_sp = self.config.max_stream_payload_bytes;
630 match connection {
631 Some(conn) if conn.close_reason().is_none() => {
632 if !bound {
633 return Err(LightningError::Handshake(format!(
634 "no authenticated route for {} at {}",
635 axon_info.hotkey, addr_key
636 )));
637 }
638 open_streaming_synapse(
639 &conn,
640 request,
641 max_fp,
642 max_sp,
643 self.config.stream_chunk_timeout,
644 )
645 .await
646 }
647 Some(conn) => {
648 let reason = conn.close_reason();
649 warn!(
650 addr = %addr_key,
651 close_reason = ?reason,
652 "QUIC connection closed, triggering reconnect (stream)"
653 );
654 self.try_reconnect_and_stream(&addr_key, &axon_info, request)
655 .await
656 }
657 None => {
658 self.try_reconnect_and_stream(&addr_key, &axon_info, request)
659 .await
660 }
661 }
662 }
663
664 async fn try_reconnect_and_query(
665 &self,
666 addr_key: &PeerAddr,
667 axon_info: &QuicAxonInfo,
668 request: QuicRequest,
669 ) -> Result<QuicResponse> {
670 let connection = self.try_reconnect(addr_key, axon_info).await?;
671 send_synapse_packet(&connection, request, self.config.max_frame_payload_bytes).await
672 }
673
674 async fn try_reconnect_and_stream(
675 &self,
676 addr_key: &PeerAddr,
677 axon_info: &QuicAxonInfo,
678 request: QuicRequest,
679 ) -> Result<StreamingResponse> {
680 let connection = self.try_reconnect(addr_key, axon_info).await?;
681 open_streaming_synapse(
682 &connection,
683 request,
684 self.config.max_frame_payload_bytes,
685 self.config.max_stream_payload_bytes,
686 self.config.stream_chunk_timeout,
687 )
688 .await
689 }
690
691 async fn try_reconnect(
692 &self,
693 addr_key: &PeerAddr,
694 axon_info: &QuicAxonInfo,
695 ) -> Result<Connection> {
696 let endpoint = self
697 .endpoint
698 .as_ref()
699 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
700 .clone();
701 let signer = self
702 .signer
703 .as_ref()
704 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
705 .clone();
706
707 {
708 let mut state = self.state.write().await;
709 if let Err(rejection) = state.registry.try_start_reconnect(
710 addr_key.clone(),
711 self.config.reconnect_max_retries,
712 self.config.reconnect_slow_probe_interval,
713 ) {
714 use crate::registry::ReconnectRejection;
715 return match rejection {
716 ReconnectRejection::Backoff { next } => {
717 Err(LightningError::Connection(format!(
718 "Reconnection to {} in backoff, next retry in {:?}",
719 addr_key,
720 next.saturating_duration_since(Instant::now())
721 )))
722 }
723 ReconnectRejection::Exhausted { attempts } => {
724 Err(LightningError::Connection(format!(
725 "Reconnection attempts exhausted for {} ({}/{}), awaiting registry refresh",
726 addr_key, attempts, self.config.reconnect_max_retries
727 )))
728 }
729 ReconnectRejection::InProgress => {
730 Err(LightningError::Connection(format!(
731 "Reconnection to {} already in progress",
732 addr_key
733 )))
734 }
735 };
736 }
737 }
738
739 warn!("Connection to {} dead, attempting reconnection", addr_key);
740
741 let reconnect_result = tokio::time::timeout(
742 self.config.connect_timeout,
743 connect_and_handshake(
744 endpoint,
745 axon_info.clone(),
746 self.wallet_hotkey.clone(),
747 signer.clone(),
748 self.config.max_frame_payload_bytes,
749 ),
750 )
751 .await;
752
753 let reconnect_result = match reconnect_result {
754 Ok(r) => r,
755 Err(_) => Err(LightningError::Connection(format!(
756 "Reconnection to {} timed out",
757 addr_key
758 ))),
759 };
760
761 match reconnect_result {
762 Ok(connection) => {
763 let co_located: Vec<String> = {
764 let state = self.state.read().await;
765 state
766 .registry
767 .hotkeys_at_addr(addr_key)
768 .into_iter()
769 .filter(|hk| *hk != axon_info.hotkey)
770 .collect()
771 };
772 let mut failed_hotkeys = Vec::new();
773 for hk in &co_located {
774 match tokio::time::timeout(
775 self.config.connect_timeout,
776 authenticate_handshake(
777 &connection,
778 hk,
779 &self.wallet_hotkey,
780 &signer,
781 self.config.max_frame_payload_bytes,
782 ),
783 )
784 .await
785 {
786 Ok(Ok(())) => {
787 info!(
788 "Re-authenticated co-located miner {} on reconnected {}",
789 hk, addr_key
790 );
791 }
792 Ok(Err(e)) => {
793 warn!(
794 "Re-authentication failed for co-located miner {} at {}: {}",
795 hk, addr_key, e
796 );
797 failed_hotkeys.push(hk.clone());
798 }
799 Err(_) => {
800 warn!(
801 "Re-authentication timed out for co-located miner {} at {}",
802 hk, addr_key
803 );
804 failed_hotkeys.push(hk.clone());
805 }
806 }
807 }
808
809 let mut state = self.state.write().await;
810 for hk in &failed_hotkeys {
811 state.registry.deregister(hk);
812 }
813 state.registry.register(axon_info.clone());
814 state
815 .registry
816 .set_connection(addr_key.clone(), connection.clone());
817 state.registry.remove_reconnect_state(addr_key);
818 info!("Reconnected to {}", addr_key);
819 Ok(connection)
820 }
821 Err(e) => {
822 let mut state = self.state.write().await;
823 let rs = state.registry.reconnect_state_or_insert(addr_key.clone());
824 rs.in_progress = false;
825 let shift = rs.attempts.min(20);
826 rs.attempts += 1;
827 let in_slow_probe = rs.attempts >= self.config.reconnect_max_retries;
828 if in_slow_probe {
829 if let Some(probe_interval) = self.config.reconnect_slow_probe_interval {
830 rs.next_retry_at = Instant::now() + probe_interval;
831 warn!(
832 "Slow probe to {} failed, next probe in {:?}: {}",
833 addr_key, probe_interval, e
834 );
835 }
836 } else {
837 let backoff = self
838 .config
839 .reconnect_initial_backoff
840 .checked_mul(2u32.pow(shift))
841 .map(|d| d.min(self.config.reconnect_max_backoff))
842 .unwrap_or(self.config.reconnect_max_backoff);
843 rs.next_retry_at = Instant::now() + backoff;
844 error!(
845 "Reconnection to {} failed (attempt {}/{}), next retry in {:?}: {}",
846 addr_key, rs.attempts, self.config.reconnect_max_retries, backoff, e
847 );
848 }
849 Err(e)
850 }
851 }
852 }
853
854 #[instrument(skip(self, miners), fields(miner_count = miners.len()))]
857 pub async fn update_miner_registry(&self, miners: Vec<QuicAxonInfo>) -> Result<()> {
858 let endpoint = self
859 .endpoint
860 .as_ref()
861 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
862 .clone();
863 let signer = self
864 .signer
865 .as_ref()
866 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
867 .clone();
868 update_miner_registry_inner(
869 &self.state,
870 &endpoint,
871 &self.wallet_hotkey,
872 &signer,
873 &self.config,
874 miners,
875 )
876 .await
877 }
878
879 pub async fn authenticated_hotkeys(&self) -> std::collections::HashSet<String> {
885 let state = self.state.read().await;
886 state.registry.active_hotkeys().into_iter().collect()
887 }
888
889 #[instrument(skip(self))]
891 pub async fn get_connection_stats(&self) -> Result<HashMap<String, String>> {
892 let state = self.state.read().await;
893
894 let mut stats = HashMap::new();
895 stats.insert(
896 "total_connections".to_string(),
897 state.registry.connection_count().to_string(),
898 );
899 stats.insert(
900 "active_miners".to_string(),
901 state.registry.active_miner_count().to_string(),
902 );
903
904 for addr_key in state.registry.connection_addrs() {
905 let status = match state.registry.get_connection(addr_key) {
906 Some(conn) => {
907 if let Some(reason) = conn.close_reason() {
908 format!("closed({:?})", reason)
909 } else {
910 "active".to_string()
911 }
912 }
913 None => "missing".to_string(),
914 };
915 stats.insert(format!("connection_{}", addr_key), status);
916 }
917
918 Ok(stats)
919 }
920
921 #[cfg(feature = "subtensor")]
924 pub async fn start_metagraph_monitor(
925 &self,
926 monitor_config: MetagraphMonitorConfig,
927 ) -> Result<()> {
928 if monitor_config.sync_interval.is_zero() {
929 return Err(LightningError::Config(
930 "sync_interval must be non-zero".into(),
931 ));
932 }
933
934 self.stop_metagraph_monitor().await;
935
936 let endpoint = self
937 .endpoint
938 .as_ref()
939 .ok_or_else(|| LightningError::Connection("QUIC endpoint not initialized".into()))?
940 .clone();
941 let signer = self
942 .signer
943 .as_ref()
944 .ok_or_else(|| LightningError::Signing("No signer configured".into()))?
945 .clone();
946
947 let subtensor = tokio::time::timeout(
948 Duration::from_secs(30),
949 OnlineClient::<PolkadotConfig>::from_url(&monitor_config.subtensor_endpoint),
950 )
951 .await
952 .map_err(|_| LightningError::Handler("subtensor connection timed out after 30s".into()))?
953 .map_err(|e| LightningError::Handler(format!("connecting to subtensor: {}", e)))?;
954
955 let mut metagraph = Metagraph::new(monitor_config.netuid);
956 tokio::time::timeout(Duration::from_secs(60), metagraph.sync(&subtensor))
957 .await
958 .map_err(|_| {
959 LightningError::Handler("initial metagraph sync timed out after 60s".into())
960 })??;
961
962 let miners = metagraph.quic_miners();
963 info!(
964 netuid = monitor_config.netuid,
965 miners = miners.len(),
966 "initial metagraph sync complete"
967 );
968
969 update_miner_registry_inner(
970 &self.state,
971 &endpoint,
972 &self.wallet_hotkey,
973 &signer,
974 &self.config,
975 miners,
976 )
977 .await?;
978
979 let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
980 let state = self.state.clone();
981 let wallet_hotkey = self.wallet_hotkey.clone();
982 let config = self.config.clone();
983 let sync_interval = monitor_config.sync_interval;
984 let subtensor_url = monitor_config.subtensor_endpoint.clone();
985
986 let handle = tokio::spawn(async move {
987 let mut interval = tokio::time::interval(sync_interval);
988 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
989 interval.tick().await;
990 let mut subtensor = subtensor;
991
992 loop {
993 tokio::select! {
994 _ = interval.tick() => {}
995 _ = shutdown_rx.changed() => {
996 info!("metagraph monitor shutting down");
997 return;
998 }
999 }
1000
1001 let sync_result =
1002 tokio::time::timeout(Duration::from_secs(60), metagraph.sync(&subtensor)).await;
1003
1004 let needs_reconnect = match sync_result {
1005 Ok(Ok(())) => {
1006 let miners = metagraph.quic_miners();
1007 info!(
1008 netuid = metagraph.netuid,
1009 miners = miners.len(),
1010 block = metagraph.block,
1011 "metagraph resync complete"
1012 );
1013 if let Err(e) = update_miner_registry_inner(
1014 &state,
1015 &endpoint,
1016 &wallet_hotkey,
1017 &signer,
1018 &config,
1019 miners,
1020 )
1021 .await
1022 {
1023 error!("registry update after metagraph sync failed: {}", e);
1024 }
1025 false
1026 }
1027 Ok(Err(e)) => {
1028 error!("metagraph sync failed, reconnecting to subtensor: {}", e);
1029 true
1030 }
1031 Err(_) => {
1032 error!("metagraph sync timed out after 60s, reconnecting to subtensor");
1033 true
1034 }
1035 };
1036
1037 if needs_reconnect {
1038 match tokio::time::timeout(
1039 Duration::from_secs(30),
1040 OnlineClient::<PolkadotConfig>::from_url(&subtensor_url),
1041 )
1042 .await
1043 {
1044 Ok(Ok(new_client)) => {
1045 subtensor = new_client;
1046 info!("subtensor client reconnected");
1047 }
1048 Ok(Err(e)) => {
1049 error!("subtensor reconnection failed: {}", e);
1050 }
1051 Err(_) => {
1052 error!("subtensor reconnection timed out after 30s");
1053 }
1054 }
1055 }
1056 }
1057 });
1058
1059 let mut st = self.state.write().await;
1060 st.metagraph_shutdown = Some(shutdown_tx);
1061 st.metagraph_handle = Some(handle);
1062 Ok(())
1063 }
1064
1065 #[cfg(feature = "subtensor")]
1067 pub async fn stop_metagraph_monitor(&self) {
1068 let (shutdown_tx, handle) = {
1069 let mut st = self.state.write().await;
1070 (st.metagraph_shutdown.take(), st.metagraph_handle.take())
1071 };
1072 if let Some(tx) = shutdown_tx {
1073 let _ = tx.send(true);
1074 }
1075 if let Some(mut handle) = handle {
1076 if tokio::time::timeout(Duration::from_secs(5), &mut handle)
1077 .await
1078 .is_err()
1079 {
1080 warn!("metagraph monitor did not shut down within 5s, aborting");
1081 handle.abort();
1082 let _ = handle.await;
1083 }
1084 }
1085 }
1086
1087 #[instrument(skip(self))]
1089 pub async fn close_all_connections(&self) -> Result<()> {
1090 #[cfg(feature = "subtensor")]
1091 self.stop_metagraph_monitor().await;
1092
1093 let mut state = self.state.write().await;
1094
1095 for (_, connection) in state.registry.drain_connections() {
1096 connection.close(0u32.into(), b"client_shutdown");
1097 }
1098
1099 state.registry.clear();
1100
1101 info!("All Lightning QUIC connections closed");
1102 Ok(())
1103 }
1104}
1105
1106async fn update_miner_registry_inner(
1107 state: &Arc<RwLock<ClientState>>,
1108 endpoint: &Endpoint,
1109 wallet_hotkey: &str,
1110 signer: &Arc<dyn Signer>,
1111 config: &LightningClientConfig,
1112 miners: Vec<QuicAxonInfo>,
1113) -> Result<()> {
1114 let new_by_hotkey: HashMap<String, QuicAxonInfo> = miners
1115 .iter()
1116 .map(|m| (m.hotkey.clone(), m.clone()))
1117 .collect();
1118
1119 let new_hotkeys_needing_auth: Vec<QuicAxonInfo>;
1120 let new_addrs_needing_connect: HashMap<PeerAddr, Vec<QuicAxonInfo>>;
1121 {
1122 let mut st = state.write().await;
1123
1124 let active_hotkeys = st.registry.active_hotkeys();
1125 for hotkey in active_hotkeys {
1126 if !new_by_hotkey.contains_key(&hotkey) {
1127 if let Some(miner) = st.registry.deregister(&hotkey) {
1128 let addr_key = miner.addr_key();
1129 info!("Miner {} deregistered from {}", hotkey, addr_key);
1130 if !st.registry.addr_has_hotkeys(&addr_key) {
1131 if let Some(connection) = st.registry.remove_connection(&addr_key) {
1132 connection.close(0u32.into(), b"miner_deregistered");
1133 }
1134 st.registry.remove_reconnect_state(&addr_key);
1135 }
1136 }
1137 }
1138 }
1139
1140 let active_addrs = st.registry.active_addrs();
1141 for addr_key in &active_addrs {
1142 if st.registry.remove_reconnect_state(addr_key) {
1143 info!(
1144 "Registry refresh reset reconnection backoff for {}",
1145 addr_key
1146 );
1147 }
1148 }
1149
1150 let dead_addrs: Vec<PeerAddr> = active_addrs
1151 .iter()
1152 .filter(|addr| {
1153 st.registry
1154 .get_connection(addr)
1155 .is_some_and(|c| c.close_reason().is_some())
1156 })
1157 .cloned()
1158 .collect();
1159 for addr_key in &dead_addrs {
1160 if let Some(conn) = st.registry.remove_connection(addr_key) {
1161 let hotkeys = st.registry.hotkeys_at_addr(addr_key);
1162 info!(
1163 addr = %addr_key,
1164 close_reason = ?conn.close_reason(),
1165 hotkeys = ?hotkeys,
1166 "Pruning dead connection and deregistering miners"
1167 );
1168 for hk in &hotkeys {
1169 st.registry.deregister(hk);
1170 }
1171 }
1172 }
1173
1174 for new_miner in new_by_hotkey.values() {
1175 if let Some(old_miner) = st.registry.active_miner(&new_miner.hotkey) {
1176 let old_addr = old_miner.addr_key();
1177 let new_addr = new_miner.addr_key();
1178 if old_addr != new_addr {
1179 info!(
1180 "Miner {} changed address from {} to {}",
1181 new_miner.hotkey, old_addr, new_addr
1182 );
1183 st.registry.deregister(&new_miner.hotkey);
1184 if !st.registry.addr_has_hotkeys(&old_addr) {
1185 if let Some(conn) = st.registry.remove_connection(&old_addr) {
1186 conn.close(0u32.into(), b"miner_addr_changed");
1187 }
1188 st.registry.remove_reconnect_state(&old_addr);
1189 }
1190 }
1191 }
1192 }
1193
1194 let new_hotkeys: Vec<QuicAxonInfo> = new_by_hotkey
1195 .values()
1196 .filter(|m| !st.registry.contains_active_miner(&m.hotkey))
1197 .cloned()
1198 .collect();
1199
1200 let mut need_auth = Vec::new();
1201 let mut need_connect: HashMap<PeerAddr, Vec<QuicAxonInfo>> = HashMap::new();
1202 for miner in new_hotkeys {
1203 let addr_key = miner.addr_key();
1204 if st.registry.contains_connection(&addr_key) {
1205 need_auth.push(miner);
1206 } else {
1207 need_connect.entry(addr_key).or_default().push(miner);
1208 }
1209 }
1210
1211 let active_count = st.registry.connection_count();
1212 let remaining_capacity = config.max_connections.saturating_sub(active_count);
1213 if need_connect.len() > remaining_capacity {
1214 warn!(
1215 "Connection limit ({}) reached with {} active, skipping {} of {} new addresses",
1216 config.max_connections,
1217 active_count,
1218 need_connect.len() - remaining_capacity,
1219 need_connect.len()
1220 );
1221 }
1222
1223 new_hotkeys_needing_auth = need_auth;
1224 new_addrs_needing_connect = need_connect.into_iter().take(remaining_capacity).collect();
1225 }
1226
1227 let timeout = config.connect_timeout;
1228 let max_fp = config.max_frame_payload_bytes;
1229
1230 if !new_hotkeys_needing_auth.is_empty() {
1231 let miners_with_conns: Vec<(QuicAxonInfo, Connection)> = {
1232 let st = state.read().await;
1233 new_hotkeys_needing_auth
1234 .into_iter()
1235 .filter_map(|miner| {
1236 let addr_key = miner.addr_key();
1237 st.registry
1238 .get_connection(&addr_key)
1239 .map(|conn| (miner, conn))
1240 })
1241 .collect()
1242 };
1243
1244 let mut authenticated = Vec::new();
1245 for (miner, conn) in &miners_with_conns {
1246 let addr_key = miner.addr_key();
1247 match tokio::time::timeout(
1248 timeout,
1249 authenticate_handshake(conn, &miner.hotkey, wallet_hotkey, signer, max_fp),
1250 )
1251 .await
1252 {
1253 Ok(Ok(())) => {
1254 info!(
1255 "Authenticated new miner {} on existing connection to {}",
1256 miner.hotkey, addr_key
1257 );
1258 authenticated.push(miner.clone());
1259 }
1260 Ok(Err(e)) => {
1261 warn!(
1262 "Handshake failed for new hotkey {} at {}: {}",
1263 miner.hotkey, addr_key, e
1264 );
1265 }
1266 Err(_) => {
1267 warn!(
1268 "Handshake timed out for new hotkey {} at {}",
1269 miner.hotkey, addr_key
1270 );
1271 }
1272 }
1273 }
1274
1275 let mut st = state.write().await;
1276 for miner in authenticated {
1277 st.registry.register(miner);
1278 }
1279 }
1280
1281 if !new_addrs_needing_connect.is_empty() {
1282 let mut set = tokio::task::JoinSet::new();
1283 for (addr_key, miners_at_addr) in new_addrs_needing_connect {
1284 info!(
1285 "New address detected, establishing QUIC connection: {}",
1286 addr_key
1287 );
1288 let ep = endpoint.clone();
1289 let wh = wallet_hotkey.to_string();
1290 let s = signer.clone();
1291 set.spawn(connect_and_authenticate_per_address(
1292 ep,
1293 wh,
1294 s,
1295 addr_key,
1296 miners_at_addr,
1297 timeout,
1298 max_fp,
1299 ));
1300 }
1301
1302 let mut results = Vec::new();
1303 while let Some(join_result) = set.join_next().await {
1304 match join_result {
1305 Ok((addr_key, conn_result, authenticated)) => {
1306 results.push((addr_key, conn_result, authenticated));
1307 }
1308 Err(e) => {
1309 error!("Connection task panicked: {}", e);
1310 }
1311 }
1312 }
1313
1314 let mut st = state.write().await;
1315 for (addr_key, conn_result, authenticated) in results {
1316 match conn_result {
1317 Ok(connection) => {
1318 if authenticated.is_empty() {
1319 warn!(
1320 "No hotkeys authenticated at {}, dropping connection",
1321 addr_key
1322 );
1323 connection.close(0u32.into(), b"no_authenticated_hotkeys");
1324 } else {
1325 for miner in authenticated {
1326 st.registry.register(miner);
1327 }
1328 st.registry.set_connection(addr_key, connection);
1329 }
1330 }
1331 Err(e) => {
1332 error!("Failed to connect to {}: {}", addr_key, e);
1333 }
1334 }
1335 }
1336 }
1337
1338 Ok(())
1339}
1340
1341fn get_peer_cert_fingerprint(connection: &Connection) -> Option<[u8; 32]> {
1342 let identity = connection.peer_identity()?;
1343 let certs = identity.downcast::<Vec<CertificateDer<'static>>>().ok()?;
1344 let first = certs.first()?;
1345 Some(blake2_256(first.as_ref()))
1346}
1347
1348async fn quic_connect(
1349 endpoint: &Endpoint,
1350 addr_key: &PeerAddr,
1351 server_name: &str,
1352) -> Result<Connection> {
1353 let addr: SocketAddr = addr_key
1354 .as_ref()
1355 .parse()
1356 .map_err(|e| LightningError::Connection(format!("Invalid address: {}", e)))?;
1357
1358 endpoint
1359 .connect(addr, server_name)
1360 .map_err(|e| LightningError::Connection(format!("Connection failed: {}", e)))?
1361 .await
1362 .map_err(|e| LightningError::Connection(format!("Connection handshake failed: {}", e)))
1363}
1364
1365async fn connect_and_authenticate_per_address(
1366 endpoint: Endpoint,
1367 wallet_hotkey: String,
1368 signer: Arc<dyn Signer>,
1369 addr_key: PeerAddr,
1370 miners_at_addr: Vec<QuicAxonInfo>,
1371 timeout: Duration,
1372 max_frame_payload: usize,
1373) -> (PeerAddr, Result<Connection>, Vec<QuicAxonInfo>) {
1374 let first = match miners_at_addr.first() {
1375 Some(m) => m,
1376 None => {
1377 return (
1378 addr_key,
1379 Err(LightningError::Connection("no miners for address".into())),
1380 vec![],
1381 );
1382 }
1383 };
1384
1385 let conn = match tokio::time::timeout(timeout, quic_connect(&endpoint, &addr_key, &first.ip))
1386 .await
1387 {
1388 Ok(Ok(c)) => c,
1389 Ok(Err(e)) => return (addr_key, Err(e), vec![]),
1390 Err(_) => {
1391 let err = LightningError::Connection(format!("Connection to {} timed out", addr_key));
1392 return (addr_key, Err(err), vec![]);
1393 }
1394 };
1395
1396 let mut authenticated = Vec::new();
1397 for miner in &miners_at_addr {
1398 match tokio::time::timeout(
1399 timeout,
1400 authenticate_handshake(
1401 &conn,
1402 &miner.hotkey,
1403 &wallet_hotkey,
1404 &signer,
1405 max_frame_payload,
1406 ),
1407 )
1408 .await
1409 {
1410 Ok(Ok(())) => authenticated.push(miner.clone()),
1411 Ok(Err(e)) => {
1412 warn!(
1413 "Handshake failed for hotkey {} at {}: {}",
1414 miner.hotkey, addr_key, e
1415 );
1416 }
1417 Err(_) => {
1418 warn!(
1419 "Handshake timed out for hotkey {} at {}",
1420 miner.hotkey, addr_key
1421 );
1422 }
1423 }
1424 }
1425
1426 (addr_key, Ok(conn), authenticated)
1427}
1428
1429async fn authenticate_handshake(
1430 connection: &Connection,
1431 expected_hotkey: &str,
1432 wallet_hotkey: &str,
1433 signer: &Arc<dyn Signer>,
1434 max_frame_payload: usize,
1435) -> Result<()> {
1436 let peer_cert_fp = get_peer_cert_fingerprint(connection).ok_or_else(|| {
1437 LightningError::Handshake("peer certificate not available for fingerprinting".to_string())
1438 })?;
1439 let peer_cert_fp_b64 = BASE64_STANDARD.encode(peer_cert_fp);
1440
1441 let nonce = generate_nonce();
1442 let timestamp = unix_timestamp_secs();
1443 let message = handshake_request_message(wallet_hotkey, timestamp, &nonce, &peer_cert_fp_b64);
1444 let msg_bytes = message.into_bytes();
1445 let signer_clone = signer.clone();
1446 let signature_bytes = tokio::task::spawn_blocking(move || signer_clone.sign(&msg_bytes))
1447 .await
1448 .map_err(|e| LightningError::Signing(format!("signer task failed: {}", e)))??;
1449
1450 let handshake_request = HandshakeRequest {
1451 validator_hotkey: wallet_hotkey.to_string(),
1452 timestamp,
1453 nonce: nonce.clone(),
1454 signature: BASE64_STANDARD.encode(&signature_bytes),
1455 };
1456
1457 let response = send_handshake(connection, handshake_request, max_frame_payload).await?;
1458 if !response.accepted {
1459 return Err(LightningError::Handshake(
1460 "Handshake rejected by miner".into(),
1461 ));
1462 }
1463
1464 if response.miner_hotkey != expected_hotkey {
1465 return Err(LightningError::Handshake(format!(
1466 "Miner hotkey mismatch: expected {}, got {}",
1467 expected_hotkey, response.miner_hotkey
1468 )));
1469 }
1470
1471 match response.cert_fingerprint {
1472 Some(ref resp_fp) if *resp_fp == peer_cert_fp_b64 => {}
1473 Some(_) => {
1474 return Err(LightningError::Handshake(
1475 "Cert fingerprint mismatch between TLS session and handshake response".to_string(),
1476 ));
1477 }
1478 None => {
1479 return Err(LightningError::Handshake(
1480 "Miner handshake response omitted required cert fingerprint".to_string(),
1481 ));
1482 }
1483 }
1484
1485 verify_miner_response_signature(&response, wallet_hotkey, &nonce, &peer_cert_fp_b64).await?;
1486
1487 info!("Handshake successful with miner {}", expected_hotkey);
1488 Ok(())
1489}
1490
1491async fn connect_and_handshake(
1492 endpoint: Endpoint,
1493 miner: QuicAxonInfo,
1494 wallet_hotkey: String,
1495 signer: Arc<dyn Signer>,
1496 max_frame_payload: usize,
1497) -> Result<Connection> {
1498 let addr_key = miner.addr_key();
1499 let connection = quic_connect(&endpoint, &addr_key, &miner.ip).await?;
1500 authenticate_handshake(
1501 &connection,
1502 &miner.hotkey,
1503 &wallet_hotkey,
1504 &signer,
1505 max_frame_payload,
1506 )
1507 .await?;
1508 Ok(connection)
1509}
1510
1511async fn verify_miner_response_signature(
1512 response: &HandshakeResponse,
1513 validator_hotkey: &str,
1514 nonce: &str,
1515 cert_fp_b64: &str,
1516) -> Result<()> {
1517 if response.signature.is_empty() {
1518 return Err(LightningError::Handshake(
1519 "Miner returned empty signature".to_string(),
1520 ));
1521 }
1522
1523 let expected_message = handshake_response_message(
1524 validator_hotkey,
1525 &response.miner_hotkey,
1526 response.timestamp,
1527 nonce,
1528 cert_fp_b64,
1529 );
1530
1531 let valid = crate::signing::verify_sr25519_signature(
1532 &response.miner_hotkey,
1533 &response.signature,
1534 &expected_message,
1535 )
1536 .await?;
1537
1538 if !valid {
1539 return Err(LightningError::Handshake(
1540 "Miner response signature verification failed".to_string(),
1541 ));
1542 }
1543
1544 Ok(())
1545}
1546
1547async fn send_handshake(
1548 connection: &Connection,
1549 request: HandshakeRequest,
1550 max_frame_payload: usize,
1551) -> Result<HandshakeResponse> {
1552 let (mut send, mut recv) = connection.open_bi().await.map_err(|e| {
1553 LightningError::Connection(format!("Failed to open bidirectional stream: {}", e))
1554 })?;
1555
1556 let request_bytes = rmp_serde::to_vec(&request).map_err(|e| {
1557 LightningError::Serialization(format!("Failed to serialize handshake: {}", e))
1558 })?;
1559
1560 write_frame_and_finish(&mut send, MessageType::HandshakeRequest, &request_bytes).await?;
1561
1562 let (msg_type, payload) = read_frame(&mut recv, max_frame_payload).await?;
1563 if msg_type != MessageType::HandshakeResponse {
1564 return Err(LightningError::Handshake(format!(
1565 "Expected HandshakeResponse, got {:?}",
1566 msg_type
1567 )));
1568 }
1569
1570 let response: HandshakeResponse = rmp_serde::from_slice(&payload).map_err(|e| {
1571 LightningError::Serialization(format!("Failed to parse handshake response: {}", e))
1572 })?;
1573
1574 Ok(response)
1575}
1576
1577async fn send_synapse_frame(send: &mut quinn::SendStream, request: QuicRequest) -> Result<()> {
1578 let synapse_packet = SynapsePacket {
1579 synapse_type: request.synapse_type,
1580 data: request.data,
1581 timestamp: unix_timestamp_secs(),
1582 };
1583
1584 let packet_bytes = rmp_serde::to_vec(&synapse_packet).map_err(|e| {
1585 LightningError::Serialization(format!("Failed to serialize synapse packet: {}", e))
1586 })?;
1587
1588 write_frame_and_finish(send, MessageType::SynapsePacket, &packet_bytes).await
1589}
1590
1591async fn send_synapse_packet(
1592 connection: &Connection,
1593 request: QuicRequest,
1594 max_frame_payload: usize,
1595) -> Result<QuicResponse> {
1596 let stable_id = connection.stable_id();
1597 debug!(stable_id, "send_synapse_packet: opening bi stream");
1598 let (mut send, mut recv) = connection
1599 .open_bi()
1600 .await
1601 .map_err(|e| LightningError::Connection(format!("Failed to open stream: {}", e)))?;
1602 debug!(stable_id, "send_synapse_packet: bi stream opened");
1603
1604 let start = Instant::now();
1605
1606 send_synapse_frame(&mut send, request).await?;
1607 debug!(
1608 stable_id,
1609 "send_synapse_packet: frame sent, awaiting response"
1610 );
1611
1612 let (msg_type, payload) = read_frame(&mut recv, max_frame_payload).await?;
1613 debug!(stable_id, msg_type = ?msg_type, elapsed_ms = start.elapsed().as_millis() as u64, "send_synapse_packet: response received");
1614
1615 match msg_type {
1616 MessageType::SynapseResponse => {
1617 let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
1618 let synapse_response: SynapseResponse =
1619 rmp_serde::from_slice(&payload).map_err(|e| {
1620 LightningError::Serialization(format!(
1621 "Failed to parse synapse response: {}",
1622 e
1623 ))
1624 })?;
1625
1626 Ok(QuicResponse {
1627 success: synapse_response.success,
1628 data: synapse_response.data,
1629 latency_ms,
1630 error: synapse_response.error,
1631 })
1632 }
1633 MessageType::StreamChunk => Err(LightningError::Transport(
1634 "received StreamChunk on non-streaming query; use query_axon_stream for streaming synapses".to_string(),
1635 )),
1636 other => Err(LightningError::Transport(format!(
1637 "unexpected response type: {:?}",
1638 other
1639 ))),
1640 }
1641}
1642
1643async fn open_streaming_synapse(
1644 connection: &Connection,
1645 request: QuicRequest,
1646 max_frame_payload: usize,
1647 max_stream_payload: usize,
1648 chunk_timeout: Option<Duration>,
1649) -> Result<StreamingResponse> {
1650 let (mut send, recv) = connection
1651 .open_bi()
1652 .await
1653 .map_err(|e| LightningError::Connection(format!("Failed to open stream: {}", e)))?;
1654
1655 send_synapse_frame(&mut send, request).await?;
1656
1657 Ok(StreamingResponse {
1658 recv,
1659 max_payload: max_frame_payload,
1660 max_stream_payload,
1661 chunk_timeout,
1662 })
1663}
1664
1665fn generate_nonce() -> String {
1666 use rand::Rng;
1667 let bytes: [u8; 16] = rand::thread_rng().gen();
1668 format!("{:032x}", u128::from_be_bytes(bytes))
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 use super::*;
1674 use sp_core::{crypto::Ss58Codec, sr25519, Pair};
1675
1676 const MINER_SEED: [u8; 32] = [1u8; 32];
1677 const VALIDATOR_SEED: [u8; 32] = [2u8; 32];
1678
1679 fn make_signed_response(
1680 miner_seed: [u8; 32],
1681 validator_hotkey: &str,
1682 nonce: &str,
1683 cert_fp_b64: &str,
1684 ) -> HandshakeResponse {
1685 let pair = sr25519::Pair::from_seed(&miner_seed);
1686 let miner_hotkey = pair.public().to_ss58check();
1687 let timestamp = unix_timestamp_secs();
1688 let message = handshake_response_message(
1689 validator_hotkey,
1690 &miner_hotkey,
1691 timestamp,
1692 nonce,
1693 cert_fp_b64,
1694 );
1695 let signature = pair.sign(message.as_bytes());
1696 HandshakeResponse {
1697 miner_hotkey,
1698 timestamp,
1699 signature: BASE64_STANDARD.encode(signature.0),
1700 accepted: true,
1701 connection_id: "test".to_string(),
1702 cert_fingerprint: Some(cert_fp_b64.to_string()),
1703 }
1704 }
1705
1706 fn validator_hotkey() -> String {
1707 sr25519::Pair::from_seed(&VALIDATOR_SEED)
1708 .public()
1709 .to_ss58check()
1710 }
1711
1712 #[tokio::test]
1713 async fn verify_valid_miner_signature() {
1714 let nonce = "test-nonce";
1715 let fp = "dGVzdC1mcA==";
1716 let resp = make_signed_response(MINER_SEED, &validator_hotkey(), nonce, fp);
1717 assert!(
1718 verify_miner_response_signature(&resp, &validator_hotkey(), nonce, fp)
1719 .await
1720 .is_ok()
1721 );
1722 }
1723
1724 #[tokio::test]
1725 async fn verify_rejects_empty_signature() {
1726 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1727 resp.signature = String::new();
1728 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1729 .await
1730 .unwrap_err();
1731 assert!(err.to_string().contains("empty signature"));
1732 }
1733
1734 #[tokio::test]
1735 async fn verify_rejects_invalid_base64() {
1736 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1737 resp.signature = "not-valid-base64!!!".to_string();
1738 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1739 .await
1740 .unwrap_err();
1741 assert!(err.to_string().contains("Failed to decode signature"));
1742 }
1743
1744 #[tokio::test]
1745 async fn verify_rejects_wrong_signature_length() {
1746 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1747 resp.signature = BASE64_STANDARD.encode([0u8; 32]);
1748 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1749 .await
1750 .unwrap_err();
1751 assert!(err.to_string().contains("Invalid signature length"));
1752 }
1753
1754 #[tokio::test]
1755 async fn verify_rejects_bad_ss58_address() {
1756 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), "n", "fp");
1757 resp.miner_hotkey = "not_a_valid_ss58".to_string();
1758 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "n", "fp")
1759 .await
1760 .unwrap_err();
1761 assert!(err.to_string().contains("Invalid SS58 address"));
1762 }
1763
1764 #[tokio::test]
1765 async fn verify_rejects_wrong_signer() {
1766 let nonce = "n";
1767 let fp = "fp";
1768 let mut resp = make_signed_response(MINER_SEED, &validator_hotkey(), nonce, fp);
1769 let wrong_pair = sr25519::Pair::from_seed(&[99u8; 32]);
1770 resp.miner_hotkey = wrong_pair.public().to_ss58check();
1771 let err = verify_miner_response_signature(&resp, &validator_hotkey(), nonce, fp)
1772 .await
1773 .unwrap_err();
1774 assert!(err.to_string().contains("signature verification failed"));
1775 }
1776
1777 #[tokio::test]
1778 async fn verify_rejects_tampered_nonce() {
1779 let fp = "fp";
1780 let resp = make_signed_response(MINER_SEED, &validator_hotkey(), "original-nonce", fp);
1781 let err = verify_miner_response_signature(&resp, &validator_hotkey(), "tampered-nonce", fp)
1782 .await
1783 .unwrap_err();
1784 assert!(err.to_string().contains("signature verification failed"));
1785 }
1786
1787 #[test]
1788 fn with_config_rejects_frame_payload_below_minimum() {
1789 let cfg = LightningClientConfig {
1790 max_frame_payload_bytes: 512,
1791 ..LightningClientConfig::default()
1792 };
1793 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1794 }
1795
1796 #[test]
1797 fn with_config_rejects_frame_payload_above_u32_max() {
1798 let too_big: u128 = u32::MAX as u128 + 1;
1799 let val = match usize::try_from(too_big) {
1800 Ok(v) => v,
1801 Err(_) => return,
1802 };
1803 let cfg = LightningClientConfig {
1804 max_frame_payload_bytes: val,
1805 max_stream_payload_bytes: val,
1806 ..LightningClientConfig::default()
1807 };
1808 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1809 }
1810
1811 #[test]
1812 fn with_config_rejects_stream_below_frame() {
1813 let base = LightningClientConfig::default();
1814 let cfg = LightningClientConfig {
1815 max_stream_payload_bytes: base.max_frame_payload_bytes - 1,
1816 ..base
1817 };
1818 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1819 }
1820
1821 #[test]
1822 fn with_config_rejects_zero_stream_chunk_timeout() {
1823 let cfg = LightningClientConfig {
1824 stream_chunk_timeout: Some(Duration::ZERO),
1825 ..LightningClientConfig::default()
1826 };
1827 assert!(LightningClient::with_config("hk".into(), cfg).is_err());
1828 }
1829
1830 #[test]
1831 fn with_config_default_succeeds() {
1832 assert!(
1833 LightningClient::with_config("hk".into(), LightningClientConfig::default()).is_ok()
1834 );
1835 }
1836}
1837
1838#[derive(Debug)]
1843struct AcceptAnyCertVerifier;
1844
1845impl ServerCertVerifier for AcceptAnyCertVerifier {
1846 fn verify_server_cert(
1847 &self,
1848 _end_entity: &CertificateDer<'_>,
1849 _intermediates: &[CertificateDer<'_>],
1850 _server_name: &ServerName<'_>,
1851 _ocsp_response: &[u8],
1852 _now: UnixTime,
1853 ) -> std::result::Result<ServerCertVerified, rustls::Error> {
1854 Ok(ServerCertVerified::assertion())
1855 }
1856
1857 fn verify_tls12_signature(
1858 &self,
1859 _message: &[u8],
1860 _cert: &CertificateDer<'_>,
1861 _dss: &rustls::DigitallySignedStruct,
1862 ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
1863 Err(rustls::Error::PeerIncompatible(
1864 rustls::PeerIncompatible::Tls12NotOffered,
1865 ))
1866 }
1867
1868 fn verify_tls13_signature(
1869 &self,
1870 _message: &[u8],
1871 _cert: &CertificateDer<'_>,
1872 _dss: &rustls::DigitallySignedStruct,
1873 ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
1874 Ok(HandshakeSignatureValid::assertion())
1875 }
1876
1877 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1878 rustls::crypto::ring::default_provider()
1879 .signature_verification_algorithms
1880 .supported_schemes()
1881 }
1882}