use super::{
SharedBlePool,
addr::BleAddr,
discovery::DiscoveryBuffer,
framing::FramedBleStream,
io::{self, BleScanner, BleStream},
pool::{BleConnection, ConnectionPool},
stats::BleStats,
};
use crate::identity::NodeAddr;
use crate::transport::{
PacketBuffer, PacketTx, ReceivedPacket, TransportAddr, TransportError, TransportId,
};
use secp256k1::XOnlyPublicKey;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, Semaphore};
use tokio::task::JoinSet;
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;
const CROSS_PROBE_GRACE_MS: u64 = 100;
const MAX_PENDING_CANDIDATES: usize = 64;
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)))
}
pub(super) struct AcceptLoopContext<S> {
pub(super) pool: SharedBlePool<S>,
pub(super) packet_tx: PacketTx,
pub(super) transport_id: TransportId,
pub(super) stats: Arc<BleStats>,
pub(super) local_pubkey: Option<[u8; 32]>,
pub(super) discovery_buffer: Arc<DiscoveryBuffer>,
pub(super) local_node_addr: Option<NodeAddr>,
pub(super) max_packet: u16,
}
pub(super) fn local_node_wins_outbound(local: &NodeAddr, peer: &NodeAddr) -> bool {
local < peer
}
async fn preferred_connection_arrived<S: BleStream>(
pool: &SharedBlePool<S>,
addr: &TransportAddr,
) -> bool {
tokio::time::sleep(std::time::Duration::from_millis(CROSS_PROBE_GRACE_MS)).await;
pool.lock().await.contains(addr)
}
async fn admit_inbound<S: BleStream + 'static>(stream: S, ctx: AcceptLoopContext<S>) {
let AcceptLoopContext {
pool,
packet_tx,
transport_id,
stats,
local_pubkey,
discovery_buffer,
local_node_addr,
max_packet,
} = ctx;
let addr = stream.remote_addr().clone();
let ta = addr.to_transport_addr();
let stream = FramedBleStream::new(stream, max_packet);
if pool.lock().await.contains(&ta) {
debug!(addr = %ta, "BLE inbound: already connected, skipping");
return;
}
let send_mtu = stream.send_mtu();
let recv_mtu = stream.recv_mtu();
if let Some(ref our_pubkey) = local_pubkey {
let peer_pubkey = match pubkey_exchange(&stream, our_pubkey).await {
Ok(peer_pubkey) => peer_pubkey,
Err(error) => {
debug!(addr = %ta, %error, "BLE inbound pubkey exchange failed");
return;
}
};
debug!(addr = %ta, "BLE inbound pubkey exchange complete");
if let Some(ref our_addr) = local_node_addr {
let peer_addr = NodeAddr::from_pubkey(&peer_pubkey);
if local_node_wins_outbound(our_addr, &peer_addr)
&& preferred_connection_arrived(&pool, &ta).await
{
debug!(addr = %ta, "BLE inbound tie-breaker: outbound won");
return;
}
} else {
discovery_buffer.add_peer_with_pubkey(&addr, peer_pubkey);
}
}
let stream = Arc::new(stream);
let conn = BleConnection {
stream: Arc::clone(&stream),
recv_task: None,
send_mtu,
recv_mtu,
established_at: tokio::time::Instant::now(),
is_static: false,
addr,
};
match pool.lock().await.insert(ta.clone(), conn) {
Ok(Some(evicted)) => {
stats.record_pool_eviction();
info!(addr = %ta, %evicted, "BLE inbound accepted with eviction");
}
Ok(None) => {
info!(addr = %ta, send_mtu, recv_mtu, "BLE inbound connection accepted");
}
Err(error) => {
warn!(addr = %ta, %error, "BLE pool full, inbound connection rejected");
stats.record_connection_rejected();
return;
}
}
if !attach_receive_loop(
stream,
ta,
pool,
packet_tx,
transport_id,
Arc::clone(&stats),
recv_mtu,
)
.await
{
return;
}
stats.record_connection_accepted();
}
pub(super) async fn accept_loop<A>(mut acceptor: A, ctx: AcceptLoopContext<A::Stream>)
where
A: io::BleAcceptor,
A::Stream: 'static,
{
let inbound_limit = ctx.pool.lock().await.max_connections().max(1);
let permits = Arc::new(Semaphore::new(inbound_limit));
let mut handlers = JoinSet::new();
loop {
while let Some(result) = handlers.try_join_next() {
if let Err(error) = result {
debug!(%error, "BLE inbound admission task failed");
}
}
match acceptor.accept().await {
Ok(stream) => {
let Ok(permit) = Arc::clone(&permits).try_acquire_owned() else {
ctx.stats.record_connection_rejected();
debug!("BLE inbound admission limit reached");
continue;
};
let handler_ctx = AcceptLoopContext {
pool: Arc::clone(&ctx.pool),
packet_tx: ctx.packet_tx.clone(),
transport_id: ctx.transport_id,
stats: Arc::clone(&ctx.stats),
local_pubkey: ctx.local_pubkey,
discovery_buffer: Arc::clone(&ctx.discovery_buffer),
local_node_addr: ctx.local_node_addr,
max_packet: ctx.max_packet,
};
handlers.spawn(async move {
let _permit = permit;
admit_inbound(stream, handler_ctx).await;
});
}
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::with_timestamp(
transport_id,
addr.clone(),
PacketBuffer::new(buf[..n].to_vec()),
crate::time::now_ms(),
);
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;
if pool
.get(&addr)
.is_some_and(|connection| Arc::ptr_eq(&connection.stream, &stream))
{
pool.remove(&addr);
}
}
pub(super) async fn attach_receive_loop<S: BleStream + 'static>(
stream: Arc<FramedBleStream<S>>,
addr: TransportAddr,
pool: SharedBlePool<S>,
packet_tx: PacketTx,
transport_id: TransportId,
stats: Arc<BleStats>,
recv_mtu: u16,
) -> bool {
let recv_task = tokio::spawn(receive_loop(
Arc::clone(&stream),
addr.clone(),
Arc::clone(&pool),
packet_tx,
transport_id,
stats,
recv_mtu,
));
let mut pool = pool.lock().await;
if let Some(connection) = pool.get_mut(&addr)
&& Arc::ptr_eq(&connection.stream, &stream)
{
connection.recv_task = Some(recv_task);
true
} else {
recv_task.abort();
false
}
}
pub(super) struct ScanProbeContext<I: io::BleIo> {
pub(super) io: Arc<I>,
pub(super) pool: SharedBlePool<I::Stream>,
pub(super) buffer: Arc<DiscoveryBuffer>,
pub(super) stats: Arc<BleStats>,
pub(super) local_pubkey: Option<[u8; 32]>,
pub(super) connect_timeout_ms: u64,
pub(super) cooldown_secs: u64,
pub(super) local_node_addr: Option<NodeAddr>,
pub(super) packet_tx: PacketTx,
pub(super) transport_id: TransportId,
pub(super) max_packet: u16,
}
pub(super) async fn scan_probe_loop<I: io::BleIo>(
mut scanner: I::Scanner,
ctx: ScanProbeContext<I>,
) {
let ScanProbeContext {
io,
pool,
buffer,
stats,
local_pubkey,
connect_timeout_ms,
cooldown_secs,
local_node_addr,
packet_tx,
transport_id,
max_packet,
} = ctx;
let mut last_probed: HashMap<BleAddr, tokio::time::Instant> = HashMap::new();
let mut pending_candidates: Vec<io::BleCandidate> = Vec::new();
let cooldown = std::time::Duration::from_secs(cooldown_secs.max(1));
let retry_interval = tokio::time::interval(cooldown);
tokio::pin!(retry_interval);
retry_interval.tick().await;
loop {
let candidate = tokio::select! {
result = scanner.next() => {
match result {
Some(a) => a,
None => {
debug!("BLE scanner ended");
break;
}
}
}
_ = retry_interval.tick() => {
last_probed.retain(|_, last| last.elapsed() < cooldown);
let pool_guard = pool.lock().await;
pending_candidates.retain(|candidate| {
!pool_guard.contains(&candidate.addr.to_transport_addr())
});
drop(pool_guard);
if let Some(candidate) = pending_candidates.first().cloned() {
candidate
} else {
continue;
}
}
};
let addr = candidate.addr.clone();
let psm = candidate.bootstrap.psm;
buffer.remember_bootstrap(&addr, candidate.bootstrap);
trace!(addr = %addr, psm, "BLE scan result");
stats.record_scan_result();
{
let pool_guard = pool.lock().await;
if pool_guard.contains(&addr.to_transport_addr()) {
pending_candidates.retain(|candidate| candidate.addr != addr);
continue;
}
}
if let Some(index) = pending_candidates
.iter()
.position(|pending| pending.addr == addr)
{
pending_candidates.remove(index);
} else {
if pending_candidates.len() == MAX_PENDING_CANDIDATES {
let evicted = pending_candidates.remove(0);
last_probed.remove(&evicted.addr);
}
}
pending_candidates.push(candidate.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 stream = FramedBleStream::new(stream, max_packet.min(candidate.bootstrap.max_packet));
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 !local_node_wins_outbound(our_addr, &peer_addr)
&& preferred_connection_arrived(&pool, &ta).await
{
debug!(
addr = %addr,
"BLE probe tie-breaker: yielding to peer's outbound"
);
continue;
}
}
let send_mtu = stream.send_mtu();
let recv_mtu = stream.recv_mtu();
let stream = Arc::new(stream);
let conn = BleConnection {
stream: Arc::clone(&stream),
recv_task: None,
send_mtu,
recv_mtu,
established_at: tokio::time::Instant::now(),
is_static: false,
addr: addr.clone(),
};
match pool.lock().await.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();
continue;
}
}
if !attach_receive_loop(
stream,
ta,
Arc::clone(&pool),
packet_tx.clone(),
transport_id,
Arc::clone(&stats),
recv_mtu,
)
.await
{
continue;
}
stats.record_connection_established();
pending_candidates.retain(|candidate| candidate.addr != addr);
buffer.add_peer_with_pubkey(&addr, peer_pubkey);
}
Err(e) => {
debug!(addr = %addr, error = %e, "BLE probe pubkey exchange failed");
}
}
}
}