use super::{
addr::BleAddr,
discovery::DiscoveryBuffer,
io::{self, BleScanner, BleStream},
pool::{BleConnection, ConnectionPool},
stats::BleStats,
};
use crate::identity::NodeAddr;
use crate::transport::{PacketTx, ReceivedPacket, TransportAddr, TransportError, TransportId};
use secp256k1::XOnlyPublicKey;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, info, trace, warn};
const PUBKEY_EXCHANGE_PREFIX: u8 = 0x00;
const PUBKEY_EXCHANGE_SIZE: usize = 33;
const PUBKEY_EXCHANGE_TIMEOUT_SECS: u64 = 5;
pub(super) async fn pubkey_exchange<S: BleStream>(
stream: &S,
local_pubkey: &[u8; 32],
) -> Result<XOnlyPublicKey, TransportError> {
let mut msg = [0u8; PUBKEY_EXCHANGE_SIZE];
msg[0] = PUBKEY_EXCHANGE_PREFIX;
msg[1..].copy_from_slice(local_pubkey);
stream.send(&msg).await?;
let mut buf = [0u8; PUBKEY_EXCHANGE_SIZE];
let timeout = std::time::Duration::from_secs(PUBKEY_EXCHANGE_TIMEOUT_SECS);
let n = match tokio::time::timeout(timeout, stream.recv(&mut buf)).await {
Ok(result) => result?,
Err(_) => return Err(TransportError::Timeout),
};
if n != PUBKEY_EXCHANGE_SIZE {
return Err(TransportError::RecvFailed(format!(
"pubkey exchange: expected {} bytes, got {}",
PUBKEY_EXCHANGE_SIZE, n
)));
}
if buf[0] != PUBKEY_EXCHANGE_PREFIX {
return Err(TransportError::RecvFailed(format!(
"pubkey exchange: bad prefix 0x{:02X}",
buf[0]
)));
}
XOnlyPublicKey::from_slice(&buf[1..])
.map_err(|e| TransportError::RecvFailed(format!("pubkey exchange: invalid key: {}", e)))
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn accept_loop<A>(
mut acceptor: A,
pool: Arc<Mutex<ConnectionPool<Arc<A::Stream>>>>,
packet_tx: PacketTx,
transport_id: TransportId,
stats: Arc<BleStats>,
_max_conns: usize,
local_pubkey: Option<[u8; 32]>,
discovery_buffer: Arc<DiscoveryBuffer>,
local_node_addr: Option<NodeAddr>,
) where
A: io::BleAcceptor,
A::Stream: 'static,
{
loop {
match acceptor.accept().await {
Ok(stream) => {
let addr = stream.remote_addr().clone();
let ta = addr.to_transport_addr();
{
let pool_guard = pool.lock().await;
if pool_guard.contains(&ta) {
debug!(addr = %ta, "BLE inbound: already connected, skipping");
continue;
}
}
let send_mtu = stream.send_mtu();
let recv_mtu = stream.recv_mtu();
if let Some(ref our_pubkey) = local_pubkey {
match pubkey_exchange(&stream, our_pubkey).await {
Ok(peer_pubkey) => {
debug!(addr = %ta, "BLE inbound pubkey exchange complete");
discovery_buffer.add_peer_with_pubkey(&addr, peer_pubkey);
if let Some(ref our_addr) = local_node_addr {
let peer_addr = NodeAddr::from_pubkey(&peer_pubkey);
if our_addr < &peer_addr {
debug!(
addr = %ta,
"BLE inbound tie-breaker: dropping (our addr < peer, outbound wins)"
);
continue;
}
}
}
Err(e) => {
debug!(addr = %ta, error = %e, "BLE inbound pubkey exchange failed");
continue;
}
}
}
let stream = Arc::new(stream);
let recv_task = tokio::spawn(receive_loop(
Arc::clone(&stream),
ta.clone(),
Arc::clone(&pool),
packet_tx.clone(),
transport_id,
Arc::clone(&stats),
recv_mtu,
));
let conn = BleConnection {
stream,
recv_task: Some(recv_task),
send_mtu,
recv_mtu,
established_at: tokio::time::Instant::now(),
is_static: false,
addr,
};
let mut pool_guard = pool.lock().await;
match pool_guard.insert(ta.clone(), conn) {
Ok(Some(evicted)) => {
stats.record_pool_eviction();
info!(addr = %ta, evicted = %evicted, "BLE inbound accepted (evicted peer)");
}
Ok(None) => {
info!(addr = %ta, send_mtu, recv_mtu, "BLE inbound connection accepted");
}
Err(e) => {
warn!(addr = %ta, error = %e, "BLE pool full, inbound connection rejected");
stats.record_connection_rejected();
continue;
}
}
stats.record_connection_accepted();
}
Err(e) => {
warn!(error = %e, "BLE accept error");
break;
}
}
}
}
pub(super) async fn receive_loop<S: BleStream>(
stream: Arc<S>,
addr: TransportAddr,
pool: Arc<Mutex<ConnectionPool<Arc<S>>>>,
packet_tx: PacketTx,
transport_id: TransportId,
stats: Arc<BleStats>,
recv_mtu: u16,
) {
let mut buf = vec![0u8; recv_mtu as usize];
loop {
match stream.recv(&mut buf).await {
Ok(0) => {
debug!(addr = %addr, "BLE connection closed by peer");
break;
}
Ok(n) => {
stats.record_recv(n);
let packet = ReceivedPacket::new(transport_id, addr.clone(), buf[..n].to_vec());
if packet_tx.send(packet).is_err() {
trace!("BLE packet_tx closed, stopping receive loop");
break;
}
}
Err(e) => {
debug!(addr = %addr, error = %e, "BLE receive error");
stats.record_recv_error();
break;
}
}
}
let mut pool = pool.lock().await;
pool.remove(&addr);
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn scan_probe_loop<I: io::BleIo>(
mut scanner: I::Scanner,
io: Arc<I>,
pool: Arc<Mutex<ConnectionPool<Arc<I::Stream>>>>,
buffer: Arc<DiscoveryBuffer>,
stats: Arc<BleStats>,
local_pubkey: Option<[u8; 32]>,
psm: u16,
connect_timeout_ms: u64,
cooldown_secs: u64,
local_node_addr: Option<NodeAddr>,
packet_tx: PacketTx,
transport_id: TransportId,
) {
let mut last_probed: HashMap<BleAddr, tokio::time::Instant> = HashMap::new();
let mut pending_addrs: Vec<BleAddr> = Vec::new();
let cooldown = std::time::Duration::from_secs(cooldown_secs);
let retry_interval = tokio::time::interval(std::time::Duration::from_secs(cooldown_secs));
tokio::pin!(retry_interval);
retry_interval.tick().await;
loop {
let addr = tokio::select! {
result = scanner.next() => {
match result {
Some(a) => a,
None => {
debug!("BLE scanner ended");
break;
}
}
}
_ = retry_interval.tick() => {
let pool_guard = pool.lock().await;
pending_addrs.retain(|a| !pool_guard.contains(&a.to_transport_addr()));
drop(pool_guard);
if let Some(a) = pending_addrs.first().cloned() {
a
} else {
continue;
}
}
};
trace!(addr = %addr, "BLE scan result");
stats.record_scan_result();
{
let pool_guard = pool.lock().await;
if pool_guard.contains(&addr.to_transport_addr()) {
pending_addrs.retain(|a| a != &addr);
continue;
}
}
if !pending_addrs.contains(&addr) {
pending_addrs.push(addr.clone());
}
if last_probed
.get(&addr)
.is_some_and(|last| last.elapsed() < cooldown)
{
continue;
}
last_probed.insert(addr.clone(), tokio::time::Instant::now());
let our_pubkey = match local_pubkey {
Some(pk) => pk,
None => {
buffer.add_peer(&addr);
continue;
}
};
let stream = match tokio::time::timeout(
std::time::Duration::from_millis(connect_timeout_ms),
io.connect(&addr, psm),
)
.await
{
Ok(Ok(s)) => s,
Ok(Err(e)) => {
debug!(addr = %addr, error = %e, "BLE probe connect failed");
continue;
}
Err(_) => {
debug!(addr = %addr, "BLE probe connect timeout");
stats.record_connect_timeout();
continue;
}
};
let ta = addr.to_transport_addr();
match pubkey_exchange(&stream, &our_pubkey).await {
Ok(peer_pubkey) => {
debug!(addr = %addr, "BLE probe complete");
if let Some(ref our_addr) = local_node_addr {
let peer_addr = NodeAddr::from_pubkey(&peer_pubkey);
if our_addr >= &peer_addr {
debug!(
addr = %addr,
"BLE probe tie-breaker: yielding to peer's outbound"
);
buffer.add_peer_with_pubkey(&addr, peer_pubkey);
continue;
}
}
let send_mtu = stream.send_mtu();
let recv_mtu = stream.recv_mtu();
let stream = Arc::new(stream);
let recv_task = tokio::spawn(receive_loop(
Arc::clone(&stream),
ta.clone(),
Arc::clone(&pool),
packet_tx.clone(),
transport_id,
Arc::clone(&stats),
recv_mtu,
));
let conn = BleConnection {
stream,
recv_task: Some(recv_task),
send_mtu,
recv_mtu,
established_at: tokio::time::Instant::now(),
is_static: false,
addr: addr.clone(),
};
let mut pool_guard = pool.lock().await;
match pool_guard.insert(ta.clone(), conn) {
Ok(Some(evicted)) => {
stats.record_pool_eviction();
debug!(addr = %ta, evicted = %evicted, "BLE probe promoted (evicted peer)");
}
Ok(None) => {
debug!(addr = %ta, "BLE probe promoted to pool");
}
Err(e) => {
warn!(addr = %ta, error = %e, "BLE pool full, probe connection dropped");
stats.record_connection_rejected();
}
}
drop(pool_guard);
stats.record_connection_established();
pending_addrs.retain(|a| a != &addr);
buffer.add_peer_with_pubkey(&addr, peer_pubkey);
}
Err(e) => {
debug!(addr = %addr, error = %e, "BLE probe pubkey exchange failed");
}
}
}
}