use crate::transport::{DiscoveredPeer, TransportAddr, TransportId};
use secp256k1::XOnlyPublicKey;
use std::sync::Mutex;
pub const DISCOVERY_VERSION: u8 = 0x01;
pub const FRAME_TYPE_BEACON: u8 = 0x01;
pub const FRAME_TYPE_DATA: u8 = 0x00;
pub const BEACON_SIZE: usize = 34;
const MAX_SCOPE_LEN: usize = u8::MAX as usize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Beacon {
pub pubkey: XOnlyPublicKey,
pub scope: Option<String>,
}
pub fn build_beacon(pubkey: &XOnlyPublicKey) -> [u8; BEACON_SIZE] {
let mut buf = [0u8; BEACON_SIZE];
buf[0] = FRAME_TYPE_BEACON;
buf[1] = DISCOVERY_VERSION;
buf[2..BEACON_SIZE].copy_from_slice(&pubkey.serialize());
buf
}
pub fn build_scoped_beacon(pubkey: &XOnlyPublicKey, scope: Option<&str>) -> Vec<u8> {
let mut buf = build_beacon(pubkey).to_vec();
let Some(scope) = scope.filter(|s| !s.is_empty()) else {
return buf;
};
let scope = scope.as_bytes();
let scope_len = scope.len().min(MAX_SCOPE_LEN);
buf.push(scope_len as u8);
buf.extend_from_slice(&scope[..scope_len]);
buf
}
pub fn parse_beacon(data: &[u8]) -> Option<XOnlyPublicKey> {
parse_beacon_record(data).map(|beacon| beacon.pubkey)
}
pub fn parse_beacon_record(data: &[u8]) -> Option<Beacon> {
if data.len() < BEACON_SIZE {
return None;
}
if data[0] != FRAME_TYPE_BEACON {
return None;
}
if data[1] != DISCOVERY_VERSION {
return None;
}
let pubkey = XOnlyPublicKey::from_slice(&data[2..34]).ok()?;
let scope = if data.len() > BEACON_SIZE {
let scope_len = data[BEACON_SIZE] as usize;
let scope_start = BEACON_SIZE + 1;
let scope_end = scope_start.checked_add(scope_len)?;
if data.len() < scope_end {
return None;
}
let scope = std::str::from_utf8(&data[scope_start..scope_end])
.ok()?
.to_string();
(!scope.is_empty()).then_some(scope)
} else {
None
};
Some(Beacon { pubkey, scope })
}
pub struct DiscoveryBuffer {
transport_id: TransportId,
scope_filter: Option<String>,
peers: Mutex<Vec<DiscoveredPeer>>,
}
impl DiscoveryBuffer {
pub fn new(transport_id: TransportId, scope_filter: Option<String>) -> Self {
Self {
transport_id,
scope_filter: scope_filter.filter(|s| !s.is_empty()),
peers: Mutex::new(Vec::new()),
}
}
pub fn add_peer(&self, src_mac: [u8; 6], beacon: Beacon) {
if let Some(scope_filter) = self.scope_filter.as_deref()
&& beacon.scope.as_deref() != Some(scope_filter)
{
return;
}
let addr = TransportAddr::from_bytes(&src_mac);
let peer = DiscoveredPeer::with_hint(self.transport_id, addr, beacon.pubkey);
let mut peers = self.peers.lock().unwrap();
peers.retain(|p| p.addr.as_bytes() != src_mac);
peers.push(peer);
}
pub fn take(&self) -> Vec<DiscoveredPeer> {
let mut peers = self.peers.lock().unwrap();
std::mem::take(&mut *peers)
}
}
#[cfg(test)]
mod tests {
use super::*;
use secp256k1::{Secp256k1, SecretKey};
fn test_pubkey() -> XOnlyPublicKey {
let secp = Secp256k1::new();
let sk = SecretKey::from_slice(&[0x42; 32]).unwrap();
let (xonly, _) = sk.public_key(&secp).x_only_public_key();
xonly
}
#[test]
fn test_build_parse_beacon() {
let pubkey = test_pubkey();
let beacon = build_beacon(&pubkey);
assert_eq!(beacon.len(), BEACON_SIZE);
assert_eq!(beacon[0], FRAME_TYPE_BEACON);
assert_eq!(beacon[1], DISCOVERY_VERSION);
let parsed = parse_beacon(&beacon).unwrap();
assert_eq!(parsed, pubkey);
}
#[test]
fn test_build_parse_scoped_beacon() {
let pubkey = test_pubkey();
let beacon = build_scoped_beacon(&pubkey, Some("iris-chat:host"));
let parsed = parse_beacon_record(&beacon).unwrap();
assert_eq!(parsed.pubkey, pubkey);
assert_eq!(parsed.scope.as_deref(), Some("iris-chat:host"));
assert_eq!(parse_beacon(&beacon), Some(pubkey));
}
#[test]
fn test_parse_scoped_beacon_rejects_truncated_scope() {
let pubkey = test_pubkey();
let mut beacon = build_beacon(&pubkey).to_vec();
beacon.push(9);
beacon.extend_from_slice(b"too");
assert!(parse_beacon_record(&beacon).is_none());
}
#[test]
fn test_parse_beacon_too_short() {
assert!(parse_beacon(&[0x01, 0x01]).is_none());
assert!(parse_beacon(&[]).is_none());
}
#[test]
fn test_parse_beacon_wrong_type() {
let mut beacon = build_beacon(&test_pubkey());
beacon[0] = 0x00; assert!(parse_beacon(&beacon).is_none());
}
#[test]
fn test_parse_beacon_wrong_version() {
let mut beacon = build_beacon(&test_pubkey());
beacon[1] = 0xFF;
assert!(parse_beacon(&beacon).is_none());
}
#[test]
fn test_frame_type_prefix() {
assert_eq!(FRAME_TYPE_DATA, 0x00);
assert_eq!(FRAME_TYPE_BEACON, 0x01);
}
#[test]
fn test_discovery_buffer() {
let buffer = DiscoveryBuffer::new(TransportId::new(1), None);
let pubkey = test_pubkey();
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
buffer.add_peer(
mac,
Beacon {
pubkey,
scope: None,
},
);
let peers = buffer.take();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].addr.as_bytes(), &mac);
assert_eq!(peers[0].pubkey_hint, Some(pubkey));
let peers = buffer.take();
assert!(peers.is_empty());
}
#[test]
fn test_discovery_buffer_dedup() {
let buffer = DiscoveryBuffer::new(TransportId::new(1), None);
let pubkey = test_pubkey();
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
let beacon = Beacon {
pubkey,
scope: None,
};
buffer.add_peer(mac, beacon.clone());
buffer.add_peer(mac, beacon);
let peers = buffer.take();
assert_eq!(peers.len(), 1);
}
#[test]
fn test_discovery_buffer_scope_filter() {
let buffer = DiscoveryBuffer::new(TransportId::new(1), Some("scope-a".to_string()));
let pubkey = test_pubkey();
let mac_a = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01];
let mac_b = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x02];
buffer.add_peer(
mac_a,
Beacon {
pubkey,
scope: Some("scope-b".to_string()),
},
);
buffer.add_peer(
mac_b,
Beacon {
pubkey,
scope: Some("scope-a".to_string()),
},
);
let peers = buffer.take();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].addr.as_bytes(), &mac_b);
}
}