mod dial;
#[cfg(any(feature = "discovery", feature = "mdns"))]
mod discovery;
#[cfg(feature = "mdns")]
mod mdns;
#[cfg(feature = "nat")]
mod nat;
#[cfg(feature = "pubsub")]
mod pubsub;
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub use discovery::DiscoveryError;
#[allow(unused_imports)] use minip2p_core::Multiaddr;
#[cfg(feature = "tcp")]
use minip2p_core::Protocol;
#[cfg(any(feature = "quic", feature = "tcp"))]
use minip2p_core::TransportKind;
use minip2p_core::{PeerAddr, PeerId};
#[cfg(feature = "discovery")]
pub use minip2p_discovery::{BeaconConfig, DISCOVERY_TOPIC};
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub use minip2p_discovery::{
DiscoveryConfigError, DiscoveryEvent, DiscoverySource, KnownPeer, PeerDiscoveryConfig,
};
pub use minip2p_identify::IdentifyMessage;
pub use minip2p_identity::Ed25519Keypair;
#[cfg(feature = "mdns")]
pub use minip2p_mdns::{MdnsConfig, MdnsConfigError};
#[cfg(feature = "nat")]
pub use minip2p_nat::{
ConnectId, NatConfig, NatError, NatEvent, Path, ReachabilityState, ReservationInfo,
ReservationPolicy,
};
#[cfg(feature = "tcp")]
use minip2p_platform::StdEntropy;
#[cfg(feature = "pubsub")]
pub use minip2p_pubsub::{
FLOODSUB_PROTOCOL_ID, FloodsubConfig, GossipsubConfig, MESHSUB_PROTOCOL_ID_V10,
MESHSUB_PROTOCOL_ID_V11, PublishError, PubsubConfig, PubsubConfigError, PubsubEvent,
TopicError,
};
#[cfg(feature = "quic")]
pub use minip2p_quic::QuicLimits;
#[cfg(feature = "quic")]
use minip2p_quic::{QuicEndpoint, QuicNodeConfig};
use minip2p_swarm::SwarmBuilder;
pub use minip2p_swarm::{
Deadline, DriverError as Error, PollNext, RESERVED_PROTOCOL_IDS, RUN_UNTIL_SKIP_LIMIT, Swarm,
SwarmError, SwarmEvent as Event,
};
#[cfg(feature = "tcp")]
use minip2p_tcp::{StdTcpProvider, TcpConfig, TcpTransport};
#[cfg(any(feature = "quic", feature = "tcp"))]
use minip2p_transport::ConnectionNamespace;
#[cfg(feature = "tcp")]
use minip2p_transport::Transport;
pub use minip2p_transport::{ConnectionId, StreamId, TransportError, TransportSet, WaitHandle};
#[cfg(feature = "pubsub")]
pub use pubsub::PubsubError;
const DEFAULT_AGENT_VERSION: &str = "minip2p/0.1.0";
#[cfg(feature = "nat")]
pub type EndpointTransport =
minip2p_circuit::CircuitTransport<TransportSet, minip2p_platform::StdEntropy>;
#[cfg(not(feature = "nat"))]
pub type EndpointTransport = TransportSet;
pub type EndpointSwarm = Swarm<EndpointTransport>;
pub struct Endpoint {
swarm: EndpointSwarm,
#[cfg(feature = "nat")]
nat: Option<nat::NatDriver>,
#[cfg(feature = "pubsub")]
pubsub: Option<pubsub::PubsubDriver>,
#[cfg(any(feature = "discovery", feature = "mdns"))]
discovery: Option<discovery::DiscoveryDriver>,
#[cfg(feature = "mdns")]
mdns: Option<mdns::MdnsDriver>,
#[cfg(any(feature = "nat", feature = "pubsub"))]
pending_events: std::collections::VecDeque<Event>,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] #[must_use = "handle the wake reason and drain every non-empty agent queue after DriverProgress"]
pub enum EndpointWake {
Event(Event),
DriverProgress,
Interrupted,
Deadline,
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
enum DriverPollKind {
Application,
Progress,
Interrupted,
Deadline,
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
struct DriverPoll {
kind: DriverPollKind,
event: Option<Event>,
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
impl DriverPoll {
fn application(event: Event) -> Self {
Self {
kind: DriverPollKind::Application,
event: Some(event),
}
}
fn progress() -> Self {
Self {
kind: DriverPollKind::Progress,
event: None,
}
}
fn deadline() -> Self {
Self {
kind: DriverPollKind::Deadline,
event: None,
}
}
fn interrupted() -> Self {
Self {
kind: DriverPollKind::Interrupted,
event: None,
}
}
}
impl Endpoint {
pub fn portable<E: minip2p_platform::EntropySource>(
identity: &Ed25519Keypair,
entropy: E,
) -> crate::PortableEndpointBuilder<E> {
crate::PortableEndpointBuilder::new(identity, entropy)
}
pub fn wait_handle(&self) -> WaitHandle {
minip2p_transport::BlockingTransport::wait_handle(self.swarm.transport())
}
pub fn builder() -> EndpointBuilder {
EndpointBuilder::default()
}
pub fn peer_id(&self) -> &PeerId {
self.swarm.local_peer_id()
}
pub fn listen(&mut self) -> Result<PeerAddr, Error> {
let mut addrs = self.listen_all()?;
addrs.drain(..).next().ok_or(Error::Invariant {
reason: "a successful listen reported no address",
})
}
pub fn listen_all(&mut self) -> Result<Vec<PeerAddr>, Error> {
let addrs = self.swarm.listen_on_bound_addrs()?;
#[cfg(feature = "nat")]
self.sync_nat_listen_addrs(&addrs);
Ok(addrs)
}
#[cfg(feature = "nat")]
fn sync_nat_listen_addrs(&mut self, addrs: &[PeerAddr]) {
if let Some(nat) = self.nat.as_mut() {
let transports: Vec<Multiaddr> =
addrs.iter().map(|addr| addr.transport().clone()).collect();
let validated = minip2p_core::select_direct_addrs(&transports, None, None);
nat.agent.set_listen_addrs(&validated);
}
}
pub fn dial(&mut self, addr: &PeerAddr) -> Result<Vec<ConnectionId>, Error> {
let targets = dial::targets(addr)?;
let mut ids = Vec::with_capacity(targets.len());
let mut failure = None;
for (_, target) in targets {
match self.swarm.dial(&target) {
Ok(id) => ids.push(id),
Err(error) => failure = failure.or(Some(error)),
}
}
match failure {
Some(error) if ids.is_empty() => Err(error),
_ => Ok(ids),
}
}
pub fn dial_ip4(&mut self, addr: &PeerAddr) -> Result<ConnectionId, Error> {
self.dial_family(addr, dial::Family::V4)
}
pub fn dial_ip6(&mut self, addr: &PeerAddr) -> Result<ConnectionId, Error> {
self.dial_family(addr, dial::Family::V6)
}
fn dial_family(
&mut self,
addr: &PeerAddr,
family: dial::Family,
) -> Result<ConnectionId, Error> {
let target = dial::targets(addr)?
.into_iter()
.find(|(candidate, _)| *candidate == family)
.map(|(_, target)| target)
.ok_or_else(|| TransportError::InvalidAddress {
context: "dial target",
reason: format!("{} names no {family:?} address", addr.transport()),
})?;
self.swarm.dial(&target)
}
pub fn ping(&mut self, peer_id: &PeerId) -> Result<(), Error> {
self.swarm.ping(peer_id)
}
pub fn disconnect(&mut self, peer_id: &PeerId) -> Result<(), Error> {
self.swarm.disconnect(peer_id)
}
#[cfg(feature = "nat")]
pub fn path(&self, peer_id: &PeerId) -> Option<Path> {
self.nat.as_ref().and_then(|nat| nat.path(peer_id))
}
pub fn connected_peers(&self) -> Vec<PeerId> {
self.swarm.connected_peers()
}
pub fn is_peer_ready(&self, peer_id: &PeerId) -> bool {
self.swarm.is_peer_ready(peer_id)
}
pub fn peer_info(&self, peer_id: &PeerId) -> Option<&IdentifyMessage> {
self.swarm.peer_info(peer_id)
}
pub fn add_protocol(&mut self, protocol_id: impl Into<String>) -> Result<(), Error> {
self.swarm.add_protocol(protocol_id)
}
pub fn open_stream(&mut self, peer_id: &PeerId, protocol_id: &str) -> Result<StreamId, Error> {
self.swarm.open_stream(peer_id, protocol_id)
}
pub fn open_stream_with_connection(
&mut self,
peer_id: &PeerId,
protocol_id: &str,
) -> Result<(ConnectionId, StreamId), Error> {
self.swarm.open_stream_with_connection(peer_id, protocol_id)
}
pub fn send_stream(
&mut self,
peer_id: &PeerId,
stream_id: StreamId,
data: impl Into<Vec<u8>>,
) -> Result<(), Error> {
self.swarm.send_stream(peer_id, stream_id, data.into())
}
pub fn close_stream_write(
&mut self,
peer_id: &PeerId,
stream_id: StreamId,
) -> Result<(), Error> {
self.swarm.close_stream_write(peer_id, stream_id)
}
pub fn reset_stream(&mut self, peer_id: &PeerId, stream_id: StreamId) -> Result<(), Error> {
self.swarm.reset_stream(peer_id, stream_id)
}
pub fn abandon_stream(&mut self, peer_id: &PeerId, stream_id: StreamId) -> Result<(), Error> {
let result = self.swarm.abandon_stream(peer_id, stream_id);
#[cfg(any(feature = "nat", feature = "pubsub"))]
self.pending_events
.retain(|event| !event.matches_stream(peer_id, stream_id));
result
}
pub fn poll(&mut self) -> Result<Vec<Event>, Error> {
#[cfg(any(feature = "nat", feature = "pubsub"))]
{
let polled = self.swarm.poll()?;
let mut events: Vec<Event> = self.pending_events.drain(..).collect();
for event in polled {
if !self.ingest_into_drivers(&event) {
events.push(event);
}
}
self.tick_drivers()?;
Ok(events)
}
#[cfg(not(any(feature = "nat", feature = "pubsub")))]
{
self.swarm.poll()
}
}
pub fn next_event(&mut self, deadline: impl Into<Deadline>) -> Result<Option<Event>, Error> {
let deadline = deadline.into();
#[cfg(any(feature = "nat", feature = "pubsub"))]
if self.has_drivers() {
return self.next_event_driven(deadline);
}
loop {
match self.swarm.poll_next_interruptible(deadline)? {
PollNext::Event(event) => return Ok(Some(event)),
PollNext::Deadline => return Ok(None),
PollNext::Interrupted => {}
}
}
}
pub fn next_wake(&mut self, deadline: impl Into<Deadline>) -> Result<EndpointWake, Error> {
let deadline = deadline.into();
#[cfg(any(feature = "nat", feature = "pubsub"))]
if self.has_drivers() {
if let Some(event) = self.pending_events.pop_front() {
return Ok(EndpointWake::Event(event));
}
if self.driver_events_len() > 0 {
return Ok(EndpointWake::DriverProgress);
}
let mut expired_poll_used = false;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
return Ok(match poll.kind {
DriverPollKind::Application => {
EndpointWake::Event(poll.event.expect("application poll carries event"))
}
DriverPollKind::Progress => EndpointWake::DriverProgress,
DriverPollKind::Interrupted => EndpointWake::Interrupted,
DriverPollKind::Deadline => EndpointWake::Deadline,
});
}
self.swarm
.poll_next_interruptible(deadline)
.map(|event| match event {
PollNext::Event(event) => EndpointWake::Event(event),
PollNext::Deadline => EndpointWake::Deadline,
PollNext::Interrupted => EndpointWake::Interrupted,
})
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn has_drivers(&self) -> bool {
#[cfg(any(feature = "discovery", feature = "mdns"))]
if self.discovery.is_some() {
return true;
}
#[cfg(feature = "mdns")]
if self.mdns.is_some() {
return true;
}
#[cfg(feature = "nat")]
if self.nat.is_some() {
return true;
}
#[cfg(feature = "pubsub")]
if self.pubsub.is_some() {
return true;
}
false
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn ingest_into_drivers(&mut self, event: &Event) -> bool {
#[cfg(any(feature = "discovery", feature = "mdns"))]
if let Some(discovery) = self.discovery.as_mut() {
discovery.observe(event, &self.swarm);
}
let mut claimed = false;
#[cfg(feature = "nat")]
if let Some(nat) = self.nat.as_mut() {
claimed = nat.ingest(event, &mut self.swarm);
}
#[cfg(feature = "pubsub")]
if !claimed && let Some(pubsub) = self.pubsub.as_mut() {
claimed = pubsub.ingest(event, &mut self.swarm);
}
claimed
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn tick_drivers(&mut self) -> Result<(), Error> {
#[cfg(feature = "nat")]
if let Some(nat) = self.nat.as_mut() {
nat.tick(&mut self.swarm);
}
#[cfg(feature = "pubsub")]
if let Some(pubsub) = self.pubsub.as_mut() {
pubsub.tick(&mut self.swarm);
}
#[cfg(feature = "mdns")]
if let Some(mdns) = self.mdns.as_mut() {
mdns.tick(self.swarm.core().local_addresses())
.map_err(mdns_driver_error)?;
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
if let (Some(discovery), Some(nat)) = (self.discovery.as_mut(), self.nat.as_mut()) {
discovery.sweep(
#[cfg(feature = "discovery")]
self.pubsub.as_mut(),
#[cfg(feature = "mdns")]
self.mdns.as_mut(),
nat,
&mut self.swarm,
);
}
Ok(())
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn driver_events_len(&self) -> usize {
let mut len = 0;
#[cfg(feature = "nat")]
if let Some(nat) = self.nat.as_ref() {
len += nat.events.len();
}
#[cfg(feature = "pubsub")]
if let Some(pubsub) = self.pubsub.as_ref() {
len += pubsub.events.len();
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
if let Some(discovery) = self.discovery.as_ref() {
len += discovery.book.pending_event_count();
}
len
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn driver_step_deadline(&self, deadline: Deadline) -> Deadline {
let mut step = deadline;
#[cfg(feature = "nat")]
if let Some(nat) = self.nat.as_ref()
&& let Some(ms) = nat.agent.next_timeout(nat.now().mono_ms)
{
step = step.earliest(Deadline::from(std::time::Duration::from_millis(ms.max(1))));
}
#[cfg(feature = "pubsub")]
if let Some(pubsub) = self.pubsub.as_ref()
&& let Some(ms) = pubsub.agent.next_timeout(pubsub.now_ms())
{
step = step.earliest(Deadline::from(std::time::Duration::from_millis(ms.max(1))));
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
if let Some(discovery) = self.discovery.as_ref()
&& let Some(ms) = discovery.next_timeout(discovery.now_ms())
{
step = step.earliest(Deadline::from(std::time::Duration::from_millis(ms.max(1))));
}
#[cfg(feature = "mdns")]
if let Some(mdns) = self.mdns.as_ref()
&& let Some(ms) = mdns.next_timeout(mdns.now_ms())
{
step = step.earliest(Deadline::from(std::time::Duration::from_millis(ms.max(1))));
}
step
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn next_event_driven(&mut self, deadline: Deadline) -> Result<Option<Event>, Error> {
if let Some(event) = self.pending_events.pop_front() {
return Ok(Some(event));
}
let mut expired_poll_used = false;
loop {
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => return Ok(poll.event),
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn poll_new_event_driven(
&mut self,
deadline: Deadline,
expired_poll_used: &mut bool,
) -> Result<DriverPoll, Error> {
loop {
if deadline.has_passed() {
if *expired_poll_used {
return Ok(DriverPoll::deadline());
}
*expired_poll_used = true;
}
let step = self.driver_step_deadline(deadline);
let polled = self.swarm.poll_next_interruptible(step)?;
if deadline.has_passed() {
*expired_poll_used = true;
}
let events_before = self.driver_events_len();
match polled {
PollNext::Event(event) => {
let consumed = self.ingest_into_drivers(&event);
self.tick_drivers()?;
if !consumed {
return Ok(DriverPoll::application(event));
}
if self.driver_events_len() > events_before {
return Ok(DriverPoll::progress());
}
}
PollNext::Deadline => {
self.tick_drivers()?;
if self.driver_events_len() > events_before {
return Ok(DriverPoll::progress());
}
if deadline.has_passed() {
return Ok(DriverPoll::deadline());
}
}
PollNext::Interrupted => return Ok(DriverPoll::interrupted()),
}
}
}
pub fn wait_peer_ready(
&mut self,
peer_id: &PeerId,
deadline: impl Into<Deadline>,
) -> Result<Option<Event>, Error> {
let deadline = deadline.into();
#[cfg(any(feature = "nat", feature = "pubsub"))]
if self.has_drivers() {
return self.wait_for_event_driven(deadline, |event| {
matches!(event, Event::PeerReady { peer_id: ready, .. } if ready == peer_id)
});
}
self.swarm.run_until(
deadline,
|event| matches!(event, Event::PeerReady { peer_id: ready, .. } if ready == peer_id),
)
}
pub fn wait_ping_rtt(
&mut self,
peer_id: &PeerId,
deadline: impl Into<Deadline>,
) -> Result<Option<u64>, Error> {
let deadline = deadline.into();
#[cfg(any(feature = "nat", feature = "pubsub"))]
let event = if self.has_drivers() {
self.wait_for_event_driven(deadline, |event| {
matches!(event, Event::PingRttMeasured { peer_id: ready, .. } if ready == peer_id)
})?
} else {
self.swarm.run_until(deadline, |event| {
matches!(event, Event::PingRttMeasured { peer_id: ready, .. } if ready == peer_id)
})?
};
#[cfg(not(any(feature = "nat", feature = "pubsub")))]
let event = self.swarm.run_until(deadline, |event| {
matches!(event, Event::PingRttMeasured { peer_id: ready, .. } if ready == peer_id)
})?;
Ok(match event {
Some(Event::PingRttMeasured { rtt_ms, .. }) => Some(rtt_ms),
_ => None,
})
}
#[cfg(feature = "nat")]
pub fn connect(&mut self, peer: &PeerId) -> Result<ConnectId, Error> {
self.connect_with_addrs(peer.clone(), Vec::new())
}
#[cfg(feature = "nat")]
pub fn connect_with_addrs(
&mut self,
peer: PeerId,
direct_addrs: Vec<Multiaddr>,
) -> Result<ConnectId, Error> {
let Some(nat) = self.nat.as_mut() else {
return Err(Error::Invariant {
reason: "NAT traversal is not configured; use EndpointBuilder::relay / nat_config",
});
};
let now = nat.now();
let id = nat.agent.connect(peer, direct_addrs, now);
nat.pump(&mut self.swarm);
Ok(id)
}
#[cfg(feature = "nat")]
pub fn connect_addr(&mut self, addr: &PeerAddr) -> Result<ConnectId, Error> {
self.connect_with_addrs(addr.peer_id().clone(), vec![addr.transport().clone()])
}
#[cfg(feature = "nat")]
pub fn cancel_connect(&mut self, id: ConnectId) {
if let Some(nat) = self.nat.as_mut() {
let now = nat.now();
nat.agent.cancel(id, now);
nat.pump(&mut self.swarm);
}
}
#[cfg(feature = "nat")]
pub fn wait_path(
&mut self,
id: ConnectId,
deadline: impl Into<Deadline>,
) -> Result<Option<Path>, Error> {
let deadline = deadline.into();
let mut expired_poll_used = false;
loop {
{
let Some(nat) = self.nat.as_mut() else {
return Err(Error::Invariant {
reason: "NAT traversal is not configured",
});
};
if let Some(index) = nat.events.iter().position(|event| {
matches!(
event,
NatEvent::PathEstablished { connect_id, .. } if *connect_id == id
)
}) {
let Some(NatEvent::PathEstablished { path, .. }) = nat.events.remove(index)
else {
unreachable!("position matched PathEstablished");
};
return Ok(Some(path));
}
if nat.events.iter().any(|event| {
matches!(
event,
NatEvent::ConnectFailed { connect_id, .. } if *connect_id == id
)
}) {
return Ok(None);
}
}
self.ensure_pending_event_capacity()?;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => self
.pending_events
.push_back(poll.event.expect("application poll carries event")),
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
#[cfg(feature = "nat")]
pub fn take_nat_events(&mut self) -> Vec<NatEvent> {
match self.nat.as_mut() {
Some(nat) => nat.events.drain(..).collect(),
None => Vec::new(),
}
}
#[cfg(feature = "nat")]
pub fn next_nat_event(
&mut self,
deadline: impl Into<Deadline>,
) -> Result<Option<NatEvent>, Error> {
let deadline = deadline.into();
let mut expired_poll_used = false;
loop {
match self.nat.as_mut() {
Some(nat) => {
if let Some(event) = nat.events.pop_front() {
return Ok(Some(event));
}
}
None => return Ok(None),
}
self.ensure_pending_event_capacity()?;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => self
.pending_events
.push_back(poll.event.expect("application poll carries event")),
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn wait_for_event_driven<F>(
&mut self,
deadline: Deadline,
mut predicate: F,
) -> Result<Option<Event>, Error>
where
F: FnMut(&Event) -> bool,
{
if let Some(index) = self.pending_events.iter().position(&mut predicate) {
return Ok(self.pending_events.remove(index));
}
let mut expired_poll_used = false;
loop {
self.ensure_pending_event_capacity()?;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => {
let event = poll.event.expect("application poll carries event");
if predicate(&event) {
return Ok(Some(event));
}
self.pending_events.push_back(event);
}
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
#[cfg(any(feature = "nat", feature = "pubsub"))]
fn ensure_pending_event_capacity(&self) -> Result<(), Error> {
if self.pending_events.len() >= RUN_UNTIL_SKIP_LIMIT {
return Err(Error::EventBacklogExceeded {
limit: RUN_UNTIL_SKIP_LIMIT,
});
}
Ok(())
}
#[cfg(feature = "nat")]
pub fn reachability(&self) -> ReachabilityState {
self.nat
.as_ref()
.map(|nat| nat.agent.reachability())
.unwrap_or_default()
}
#[cfg(feature = "nat")]
pub fn active_reservation(&self) -> Option<ReservationInfo> {
self.nat
.as_ref()
.and_then(|nat| nat.agent.active_reservation().cloned())
}
#[cfg(feature = "pubsub")]
pub fn subscribe(&mut self, topic: &str) -> Result<bool, PubsubError> {
let Some(pubsub) = self.pubsub.as_mut() else {
return Err(PubsubError::NotEnabled);
};
let now_ms = pubsub.now_ms();
let newly = pubsub.agent.subscribe(topic, now_ms)?;
pubsub.pump(&mut self.swarm);
Ok(newly)
}
#[cfg(feature = "pubsub")]
pub fn unsubscribe(&mut self, topic: &str) -> Result<bool, PubsubError> {
#[cfg(feature = "discovery")]
if self
.discovery
.as_ref()
.is_some_and(|discovery| discovery.topic() == Some(topic))
{
return Err(PubsubError::DiscoveryTopicReserved);
}
let Some(pubsub) = self.pubsub.as_mut() else {
return Err(PubsubError::NotEnabled);
};
let now_ms = pubsub.now_ms();
let removed = pubsub.agent.unsubscribe(topic, now_ms);
pubsub.pump(&mut self.swarm);
Ok(removed)
}
#[cfg(feature = "pubsub")]
pub fn publish(&mut self, topic: &str, data: impl Into<Vec<u8>>) -> Result<(), PubsubError> {
let Some(pubsub) = self.pubsub.as_mut() else {
return Err(PubsubError::NotEnabled);
};
let now_ms = pubsub.now_ms();
pubsub.agent.publish(topic, data.into(), now_ms)?;
pubsub.pump(&mut self.swarm);
Ok(())
}
#[cfg(feature = "pubsub")]
pub fn take_pubsub_events(&mut self) -> Vec<PubsubEvent> {
match self.pubsub.as_mut() {
Some(pubsub) => pubsub.events.drain(..).collect(),
None => Vec::new(),
}
}
#[cfg(feature = "pubsub")]
pub fn next_pubsub_event(
&mut self,
deadline: impl Into<Deadline>,
) -> Result<Option<PubsubEvent>, PubsubError> {
let deadline = deadline.into();
let mut expired_poll_used = false;
loop {
match self.pubsub.as_mut() {
Some(pubsub) => {
if let Some(event) = pubsub.events.pop_front() {
return Ok(Some(event));
}
}
None => return Err(PubsubError::NotEnabled),
}
self.ensure_pending_event_capacity()?;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => self
.pending_events
.push_back(poll.event.expect("application poll carries event")),
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub fn known_peers(&self) -> Vec<KnownPeer> {
self.discovery
.as_ref()
.map(|driver| driver.book.known_peers())
.unwrap_or_default()
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub fn discovery_now_ms(&self) -> Option<u64> {
self.discovery
.as_ref()
.map(discovery::DiscoveryDriver::now_ms)
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub fn take_discovery_events(&mut self) -> Vec<DiscoveryEvent> {
self.discovery
.as_mut()
.map(|driver| {
let mut events = Vec::new();
while let Some(event) = driver.book.poll_event() {
events.push(event);
}
events
})
.unwrap_or_default()
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub fn next_discovery_event(
&mut self,
deadline: impl Into<Deadline>,
) -> Result<Option<DiscoveryEvent>, DiscoveryError> {
let deadline = deadline.into();
let mut expired_poll_used = false;
loop {
match self.discovery.as_mut() {
Some(discovery) => {
if let Some(event) = discovery.book.poll_event() {
return Ok(Some(event));
}
}
None => return Err(DiscoveryError::NotEnabled),
}
self.ensure_pending_event_capacity()?;
let poll = self.poll_new_event_driven(deadline, &mut expired_poll_used)?;
match poll.kind {
DriverPollKind::Application => self
.pending_events
.push_back(poll.event.expect("application poll carries event")),
DriverPollKind::Progress => {}
DriverPollKind::Interrupted => {}
DriverPollKind::Deadline => return Ok(None),
}
}
}
pub fn swarm(&self) -> &EndpointSwarm {
&self.swarm
}
pub fn swarm_mut(&mut self) -> &mut EndpointSwarm {
&mut self.swarm
}
#[cfg(feature = "mdns")]
pub fn shutdown(&mut self) -> Result<(), Error> {
let result = self
.mdns
.as_mut()
.map(mdns::MdnsDriver::shutdown)
.transpose()
.map(|_| ())
.map_err(mdns_driver_error);
if let (Some(discovery), Some(nat)) = (self.discovery.as_mut(), self.nat.as_mut()) {
discovery.shutdown(nat, &mut self.swarm);
}
result
}
}
#[cfg(feature = "mdns")]
fn mdns_driver_error(error: minip2p_mdns::MdnsError) -> Error {
TransportError::PollError {
reason: error.to_string(),
}
.into()
}
#[cfg(feature = "mdns")]
fn mdns_seed(keypair: &Ed25519Keypair) -> [u8; 32] {
let mut seed = [0u8; 32];
let peer_id = keypair.peer_id();
let digest = peer_id.digest_bytes();
for (index, byte) in digest.iter().enumerate() {
seed[index % seed.len()] ^= *byte;
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0)
.to_le_bytes();
for (index, byte) in seed.iter_mut().enumerate() {
*byte ^= timestamp[index % timestamp.len()];
}
seed
}
enum Bind {
#[cfg(feature = "quic")]
Quic(QuicBind),
#[cfg(feature = "tcp")]
Tcp(TcpBind),
}
#[cfg(feature = "quic")]
enum QuicBind {
Addr(String),
Multiaddr(Multiaddr),
DualMultiaddr(Multiaddr, Multiaddr),
DualStack,
}
#[cfg(feature = "tcp")]
enum TcpBind {
Addr(String),
Multiaddr(Multiaddr),
}
pub struct EndpointBuilder {
keypair: Option<Ed25519Keypair>,
agent_version: String,
#[cfg(feature = "quic")]
quic_limits: QuicLimits,
#[cfg(feature = "tcp")]
tcp_config: TcpConfig,
binds: Vec<Bind>,
protocols: Vec<String>,
#[cfg(feature = "nat")]
nat_config: Option<NatConfig>,
#[cfg(feature = "nat")]
relays: Vec<PeerAddr>,
#[cfg(feature = "nat")]
autonat_servers: Vec<PeerAddr>,
#[cfg(feature = "pubsub")]
pubsub_config: Option<PubsubConfig>,
#[cfg(feature = "discovery")]
discovery_config: Option<BeaconConfig>,
#[cfg(feature = "mdns")]
mdns_config: Option<MdnsConfig>,
#[cfg(any(feature = "discovery", feature = "mdns"))]
peer_discovery_config: PeerDiscoveryConfig,
}
impl Default for EndpointBuilder {
fn default() -> Self {
Self {
keypair: None,
agent_version: DEFAULT_AGENT_VERSION.to_string(),
#[cfg(feature = "quic")]
quic_limits: QuicLimits::default(),
#[cfg(feature = "tcp")]
tcp_config: TcpConfig::default(),
binds: Vec::new(),
protocols: Vec::new(),
#[cfg(feature = "nat")]
nat_config: None,
#[cfg(feature = "nat")]
relays: Vec::new(),
#[cfg(feature = "nat")]
autonat_servers: Vec::new(),
#[cfg(feature = "pubsub")]
pubsub_config: None,
#[cfg(feature = "discovery")]
discovery_config: None,
#[cfg(feature = "mdns")]
mdns_config: None,
#[cfg(any(feature = "discovery", feature = "mdns"))]
peer_discovery_config: PeerDiscoveryConfig::default(),
}
}
}
struct BuilderParts {
keypair: Ed25519Keypair,
agent_version: String,
#[cfg(feature = "quic")]
quic_limits: QuicLimits,
#[cfg(feature = "tcp")]
tcp_config: TcpConfig,
binds: Vec<Bind>,
protocols: Vec<String>,
#[cfg(feature = "nat")]
nat_config: Option<NatConfig>,
#[cfg(feature = "pubsub")]
pubsub_config: Option<PubsubConfig>,
#[cfg(feature = "discovery")]
discovery_config: Option<BeaconConfig>,
#[cfg(feature = "mdns")]
mdns_config: Option<MdnsConfig>,
#[cfg(any(feature = "discovery", feature = "mdns"))]
peer_discovery_config: PeerDiscoveryConfig,
}
impl EndpointBuilder {
pub fn identity(mut self, keypair: Ed25519Keypair) -> Self {
self.keypair = Some(keypair);
self
}
pub fn agent_version(mut self, value: impl Into<String>) -> Self {
self.agent_version = value.into();
self
}
#[cfg(feature = "quic")]
pub fn quic_limits(mut self, limits: QuicLimits) -> Self {
self.quic_limits = limits;
self
}
#[cfg(feature = "tcp")]
pub fn tcp_config(mut self, config: TcpConfig) -> Self {
self.tcp_config = config;
self
}
#[cfg(feature = "quic")]
pub fn quic(mut self, bind_addr: impl Into<String>) -> Self {
self.binds
.push(Bind::Quic(QuicBind::Addr(bind_addr.into())));
self
}
#[cfg(feature = "quic")]
pub fn quic_multiaddr(mut self, addr: &Multiaddr) -> Self {
self.binds
.push(Bind::Quic(QuicBind::Multiaddr(addr.clone())));
self
}
#[cfg(feature = "quic")]
pub fn quic_dual_multiaddr(mut self, first: &Multiaddr, second: &Multiaddr) -> Self {
self.binds.push(Bind::Quic(QuicBind::DualMultiaddr(
first.clone(),
second.clone(),
)));
self
}
#[cfg(feature = "quic")]
pub fn quic_dual_stack(mut self) -> Self {
self.binds.push(Bind::Quic(QuicBind::DualStack));
self
}
#[cfg(feature = "tcp")]
pub fn tcp(mut self, bind_addr: impl Into<String>) -> Self {
self.binds.push(Bind::Tcp(TcpBind::Addr(bind_addr.into())));
self
}
#[cfg(feature = "tcp")]
pub fn tcp_multiaddr(mut self, addr: &Multiaddr) -> Self {
self.binds.push(Bind::Tcp(TcpBind::Multiaddr(addr.clone())));
self
}
pub fn protocol(mut self, protocol_id: impl Into<String>) -> Self {
let id = protocol_id.into();
if !self.protocols.iter().any(|protocol| protocol == &id) {
self.protocols.push(id);
}
self
}
#[cfg(feature = "nat")]
pub fn relay(mut self, relay: PeerAddr) -> Self {
self.relays.push(relay);
self
}
#[cfg(feature = "nat")]
pub fn autonat_server(mut self, server: PeerAddr) -> Self {
self.autonat_servers.push(server);
self
}
#[cfg(feature = "nat")]
pub fn nat_config(mut self, config: NatConfig) -> Self {
self.nat_config = Some(config);
self
}
#[cfg(feature = "pubsub")]
pub fn pubsub(mut self) -> Self {
self.pubsub_config.get_or_insert_with(PubsubConfig::default);
self
}
#[cfg(feature = "pubsub")]
pub fn pubsub_config(mut self, config: impl Into<PubsubConfig>) -> Self {
self.pubsub_config = Some(config.into());
self
}
#[cfg(feature = "discovery")]
pub fn discovery(mut self) -> Self {
self.pubsub_config.get_or_insert_with(PubsubConfig::default);
self.discovery_config = Some(BeaconConfig::default());
self
}
#[cfg(feature = "discovery")]
pub fn discovery_config(mut self, config: BeaconConfig) -> Result<Self, DiscoveryConfigError> {
config.validate()?;
self.pubsub_config.get_or_insert_with(PubsubConfig::default);
self.discovery_config = Some(config);
Ok(self)
}
#[cfg(feature = "mdns")]
pub fn mdns(mut self) -> Self {
self.mdns_config = Some(MdnsConfig::default());
self
}
#[cfg(feature = "mdns")]
pub fn mdns_config(mut self, config: MdnsConfig) -> Result<Self, MdnsConfigError> {
config.validate()?;
self.mdns_config = Some(config);
Ok(self)
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
pub fn peer_discovery_config(
mut self,
config: PeerDiscoveryConfig,
) -> Result<Self, DiscoveryConfigError> {
config.validate()?;
self.peer_discovery_config = config;
Ok(self)
}
pub fn bind(self) -> Result<Endpoint, Error> {
let parts = self.into_parts()?;
let transport = bind_transports(&parts)?;
build_endpoint(parts, transport)
}
#[cfg(feature = "quic")]
pub fn bind_quic(self, bind_addr: impl Into<String>) -> Result<Endpoint, Error> {
self.quic(bind_addr).bind()
}
#[cfg(feature = "quic")]
pub fn bind_quic_multiaddr(self, addr: &Multiaddr) -> Result<Endpoint, Error> {
self.quic_multiaddr(addr).bind()
}
#[cfg(feature = "quic")]
pub fn bind_quic_dual_stack(self) -> Result<Endpoint, Error> {
self.quic_dual_stack().bind()
}
#[cfg(feature = "tcp")]
pub fn bind_tcp(self, bind_addr: impl Into<String>) -> Result<Endpoint, Error> {
self.tcp(bind_addr).bind()
}
fn into_parts(self) -> Result<BuilderParts, Error> {
if let Some(protocol) = self
.protocols
.iter()
.find(|protocol| RESERVED_PROTOCOL_IDS.contains(&protocol.as_str()))
{
return Err(SwarmError::ReservedProtocol {
protocol_id: protocol.clone(),
}
.into());
}
#[cfg(feature = "pubsub")]
if let Some(config) = &self.pubsub_config {
config
.validate()
.map_err(|error| TransportError::InvalidConfig {
reason: error.to_string(),
})?;
}
#[cfg(feature = "nat")]
let nat_config = {
let enabled = self.nat_config.is_some()
|| !self.relays.is_empty()
|| !self.autonat_servers.is_empty()
|| {
#[cfg(feature = "discovery")]
{
self.discovery_config.is_some()
}
#[cfg(not(feature = "discovery"))]
{
false
}
}
|| {
#[cfg(feature = "mdns")]
{
self.mdns_config.is_some()
}
#[cfg(not(feature = "mdns"))]
{
false
}
};
enabled.then(|| {
let mut config = self.nat_config.unwrap_or_default();
config.relays.extend(self.relays);
config.autonat_servers.extend(self.autonat_servers);
config
})
};
Ok(BuilderParts {
keypair: self.keypair.unwrap_or_else(Ed25519Keypair::generate),
agent_version: self.agent_version,
#[cfg(feature = "quic")]
quic_limits: self.quic_limits,
#[cfg(feature = "tcp")]
tcp_config: self.tcp_config,
binds: self.binds,
protocols: self.protocols,
#[cfg(feature = "nat")]
nat_config,
#[cfg(feature = "pubsub")]
pubsub_config: self.pubsub_config,
#[cfg(feature = "discovery")]
discovery_config: self.discovery_config,
#[cfg(feature = "mdns")]
mdns_config: self.mdns_config,
#[cfg(any(feature = "discovery", feature = "mdns"))]
peer_discovery_config: self.peer_discovery_config,
})
}
}
#[cfg(feature = "tcp")]
fn tcp_bind_addrs(spec: &str) -> Result<Vec<Multiaddr>, Error> {
use std::net::ToSocketAddrs;
let resolved = spec
.to_socket_addrs()
.map_err(|error| TransportError::InvalidAddress {
context: "tcp bind address",
reason: format!("{spec} is not a bindable address: {error}"),
})?;
let addrs = tcp_addrs_of(resolved);
if addrs.is_empty() {
return Err(TransportError::InvalidAddress {
context: "tcp bind address",
reason: format!("{spec} resolved to no address"),
}
.into());
}
Ok(addrs)
}
#[cfg(feature = "tcp")]
fn tcp_addrs_of(resolved: impl IntoIterator<Item = std::net::SocketAddr>) -> Vec<Multiaddr> {
let mut addrs: Vec<Multiaddr> = Vec::new();
for addr in resolved {
let host = match addr.ip() {
std::net::IpAddr::V4(v4) => Protocol::Ip4(v4.octets()),
std::net::IpAddr::V6(v6) => Protocol::Ip6(v6.octets()),
};
let addr = Multiaddr::from_protocols(vec![host, Protocol::Tcp(addr.port())]);
if !addrs.contains(&addr) {
addrs.push(addr);
}
}
addrs
}
fn bind_transports(parts: &BuilderParts) -> Result<TransportSet, Error> {
#[allow(unused_mut)] let mut set = TransportSet::new();
#[cfg(feature = "tcp")]
let mut tcp_bound = false;
for bind in &parts.binds {
match bind {
#[cfg(feature = "quic")]
Bind::Quic(spec) => {
let config = QuicNodeConfig::new(parts.keypair.clone())
.with_limits(parts.quic_limits.clone());
let transport = match spec {
QuicBind::Addr(addr) => QuicEndpoint::bind(config, addr)?,
QuicBind::Multiaddr(addr) => QuicEndpoint::bind_multiaddr(config, addr)?,
QuicBind::DualMultiaddr(first, second) => {
QuicEndpoint::bind_dual_multiaddr(config, first, second)?
}
QuicBind::DualStack => QuicEndpoint::dual_stack(config)?,
};
let namespaces = transport.namespaces();
insert_member(&mut set, TransportKind::Quic, namespaces, transport)?;
}
#[cfg(feature = "tcp")]
Bind::Tcp(_) if tcp_bound => {}
#[cfg(feature = "tcp")]
Bind::Tcp(_) => {
tcp_bound = true;
let transport = bind_tcp_member(parts)?;
let namespace = transport.namespace();
insert_member(&mut set, TransportKind::Tcp, [namespace], transport)?;
}
#[cfg(not(any(feature = "quic", feature = "tcp")))]
_ => unreachable!("Bind has no constructible variants without a transport feature"),
}
}
if set.is_empty() {
return Err(TransportError::InvalidConfig {
reason: "an endpoint needs at least one transport to bind".into(),
}
.into());
}
Ok(set)
}
#[cfg(feature = "tcp")]
fn bind_tcp_member(
parts: &BuilderParts,
) -> Result<TcpTransport<StdTcpProvider, StdEntropy>, Error> {
let namespace = parts.tcp_config.namespace;
if namespace != ConnectionNamespace::TCP_IPV4 && namespace != ConnectionNamespace::TCP_IPV6 {
return Err(TransportError::InvalidConfig {
reason: format!(
"a tcp transport must allocate in a tcp namespace, not {namespace}; \
see TcpConfig::namespace"
),
}
.into());
}
let provider = StdTcpProvider::new().map_err(|error| TransportError::ListenFailed {
reason: error.to_string(),
})?;
let mut transport = TcpTransport::with_config(
provider,
parts.keypair.clone(),
StdEntropy::new(),
parts.tcp_config.clone(),
);
for bind in &parts.binds {
#[cfg(feature = "quic")]
let spec = match bind {
Bind::Tcp(spec) => spec,
Bind::Quic(_) => continue,
};
#[cfg(not(feature = "quic"))]
let Bind::Tcp(spec) = bind;
match spec {
TcpBind::Addr(spec) => {
for addr in tcp_bind_addrs(spec)? {
transport.listen(&addr)?;
}
}
TcpBind::Multiaddr(addr) => {
transport.listen(addr)?;
}
}
}
Ok(transport)
}
#[cfg(any(feature = "quic", feature = "tcp"))]
fn insert_member<T: minip2p_transport::BlockingTransport + Send + 'static>(
set: &mut TransportSet,
kind: TransportKind,
namespaces: impl IntoIterator<Item = ConnectionNamespace>,
transport: T,
) -> Result<(), Error> {
set.insert(kind, namespaces, Box::new(transport))
.map_err(|rejected| {
TransportError::InvalidConfig {
reason: rejected.error().to_string(),
}
.into()
})
}
fn build_endpoint(parts: BuilderParts, transport: TransportSet) -> Result<Endpoint, Error> {
let mut builder = SwarmBuilder::new(&parts.keypair).agent_version(parts.agent_version);
#[cfg(any(feature = "nat", feature = "pubsub"))]
let mut protocols = parts.protocols;
#[cfg(not(any(feature = "nat", feature = "pubsub")))]
let protocols = parts.protocols;
#[cfg(feature = "nat")]
if parts.nat_config.is_some() {
for id in [
minip2p_nat::HOP_PROTOCOL_ID,
minip2p_nat::STOP_PROTOCOL_ID,
minip2p_nat::DCUTR_PROTOCOL_ID,
minip2p_nat::AUTONAT_PROTOCOL_ID,
] {
if !protocols.iter().any(|existing| existing == id) {
protocols.push(id.to_string());
}
}
}
#[cfg(feature = "pubsub")]
if let Some(config) = &parts.pubsub_config {
for id in config.protocol_ids() {
if !protocols.iter().any(|existing| existing == id) {
protocols.push((*id).to_string());
}
}
}
for protocol in protocols {
builder = builder.protocol(protocol);
}
#[cfg(feature = "nat")]
let transport = minip2p_circuit::CircuitTransport::new_os(transport, parts.keypair.clone());
let swarm = builder.build(transport)?;
#[cfg(feature = "nat")]
let nat = parts.nat_config.map(|config| {
let relay_addrs = config
.relays
.iter()
.map(|relay| (relay.peer_id().clone(), relay.transport().clone()))
.collect();
let agent = minip2p_nat::NatAgent::new(swarm.local_peer_id().clone(), config);
nat::NatDriver::new(agent, relay_addrs)
});
#[cfg(feature = "discovery")]
let discovery_config = parts.discovery_config;
#[cfg(feature = "mdns")]
let mdns_config = parts.mdns_config;
#[cfg(any(feature = "discovery", feature = "mdns"))]
let peer_discovery_config = parts.peer_discovery_config;
#[cfg(feature = "pubsub")]
let pubsub = parts
.pubsub_config
.map(|config| -> Result<pubsub::PubsubDriver, Error> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0);
let initial_seqno = timestamp as u64;
let entropy_seed = parts
.keypair
.peer_id()
.digest_bytes()
.iter()
.fold(initial_seqno ^ (timestamp >> 64) as u64, |seed, byte| {
seed.rotate_left(5) ^ u64::from(*byte)
});
let agent = minip2p_pubsub::PubsubAgent::new(
parts.keypair.clone(),
config,
initial_seqno,
entropy_seed,
)
.map_err(|error| TransportError::InvalidConfig {
reason: error.to_string(),
})?;
Ok(pubsub::PubsubDriver::new(agent))
})
.transpose()?;
#[cfg(feature = "discovery")]
let mut pubsub = pubsub;
#[cfg(feature = "discovery")]
if let (Some(pubsub), Some(config)) = (pubsub.as_mut(), discovery_config.as_ref()) {
pubsub
.agent
.subscribe(&config.topic, 0)
.map_err(|_| Error::Invariant {
reason: "validated discovery topic was rejected by pubsub",
})?;
}
#[cfg(feature = "discovery")]
let beacon = match discovery_config {
Some(config) => Some(
minip2p_discovery::BeaconAgent::new(parts.keypair.public_key(), config).map_err(
|_| Error::Invariant {
reason: "validated beacon configuration was rejected",
},
)?,
),
None => None,
};
#[cfg(feature = "mdns")]
let mdns = match mdns_config {
Some(config) => {
let agent = minip2p_mdns::MdnsAgent::new(
parts.keypair.peer_id(),
config.clone(),
mdns_seed(&parts.keypair),
)
.map_err(|error| TransportError::InvalidConfig {
reason: error.to_string(),
})?;
let sockets = minip2p_mdns::MdnsSockets::new(&config).map_err(|error| {
TransportError::ListenFailed {
reason: error.to_string(),
}
})?;
Some(mdns::MdnsDriver::new(agent, sockets, &config))
}
None => None,
};
#[cfg(any(feature = "discovery", feature = "mdns"))]
let discovery_enabled = {
#[cfg(feature = "discovery")]
{
beacon.is_some()
}
#[cfg(not(feature = "discovery"))]
{
false
}
} || {
#[cfg(feature = "mdns")]
{
mdns.is_some()
}
#[cfg(not(feature = "mdns"))]
{
false
}
};
#[cfg(any(feature = "discovery", feature = "mdns"))]
let discovery = if discovery_enabled {
let book = minip2p_discovery::PeerDiscoveryAgent::new(
parts.keypair.peer_id(),
peer_discovery_config,
)
.map_err(|_| Error::Invariant {
reason: "validated discovery configuration was rejected",
})?;
Some(discovery::DiscoveryDriver::new(
book,
#[cfg(feature = "discovery")]
beacon,
))
} else {
None
};
Ok(Endpoint {
swarm,
#[cfg(feature = "nat")]
nat,
#[cfg(feature = "pubsub")]
pubsub,
#[cfg(any(feature = "discovery", feature = "mdns"))]
discovery,
#[cfg(feature = "mdns")]
mdns,
#[cfg(any(feature = "nat", feature = "pubsub"))]
pending_events: std::collections::VecDeque::new(),
})
}
#[cfg(all(test, feature = "quic"))]
mod tests {
use super::*;
#[cfg(feature = "tcp")]
use std::sync::Arc;
#[cfg(feature = "tcp")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "tcp")]
struct Driven {
stop: Arc<AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
}
#[cfg(feature = "tcp")]
impl Driven {
fn new(mut endpoint: Endpoint) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&stop);
let thread = std::thread::spawn(move || {
while !flag.load(Ordering::Relaxed) {
endpoint
.next_event(Duration::from_millis(20))
.expect("drive peer");
}
});
Self {
stop,
thread: Some(thread),
}
}
}
#[cfg(feature = "tcp")]
impl Drop for Driven {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
#[cfg(feature = "tcp")]
fn wait_for(
endpoint: &mut Endpoint,
what: &str,
mut wanted: impl FnMut(&Event) -> bool,
) -> Event {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
let mut seen = Vec::new();
while std::time::Instant::now() < deadline {
let Some(event) = endpoint
.next_event(Duration::from_millis(50))
.expect("drive endpoint")
else {
continue;
};
if wanted(&event) {
return event;
}
seen.push(event);
}
panic!("no {what} arrived; saw {seen:?}");
}
#[cfg(feature = "tcp")]
fn tcp_port(addr: &PeerAddr) -> u16 {
match addr.transport().protocols() {
[_, Protocol::Tcp(port)] => *port,
other => panic!("not a /tcp address: {other:?}"),
}
}
#[cfg(feature = "discovery")]
#[test]
fn discovery_config_is_rejected_before_binding() {
let config = BeaconConfig {
beacon_interval_ms: 0,
..BeaconConfig::default()
};
assert!(matches!(
Endpoint::builder().discovery_config(config),
Err(DiscoveryConfigError::ZeroBeaconInterval)
));
}
#[test]
fn an_endpoint_with_nothing_to_bind_is_refused() {
let Err(error) = Endpoint::builder().bind() else {
panic!("an endpoint with no transport must not build");
};
assert!(
format!("{error}").contains("at least one transport"),
"got {error}"
);
}
#[test]
fn one_address_shape_cannot_be_bound_twice() {
let Err(error) = Endpoint::builder()
.quic("127.0.0.1:0")
.quic("127.0.0.1:0")
.bind()
else {
panic!("one shape must not have two members");
};
assert_eq!(
format!("{error}"),
"invalid transport configuration: this set already has a transport \
for Quic addresses",
"the refusal should name the shape that was already claimed"
);
}
#[cfg(feature = "tcp")]
#[test]
fn a_tcp_endpoint_reports_the_port_it_was_given() {
let mut endpoint = Endpoint::builder()
.bind_tcp("127.0.0.1:0")
.expect("bind tcp endpoint");
let addrs = endpoint.listen_all().expect("listen");
assert_eq!(addrs.len(), 1, "one transport, one address: {addrs:?}");
let port = tcp_port(&addrs[0]);
assert_ne!(
port, 0,
"an ephemeral bind reports the port it actually got"
);
assert_eq!(
addrs[0].transport().to_string(),
format!("/ip4/127.0.0.1/tcp/{port}"),
"the host asked for is the host reported, and nothing is added to it"
);
assert_eq!(addrs[0].peer_id(), endpoint.peer_id());
}
#[cfg(feature = "tcp")]
#[test]
fn every_bound_transport_reports_where_it_listens() {
let mut endpoint = Endpoint::builder()
.quic("127.0.0.1:0")
.tcp("127.0.0.1:0")
.bind()
.expect("bind both");
let reported: Vec<String> = endpoint
.listen_all()
.expect("listen")
.iter()
.map(|addr| addr.transport().to_string())
.collect();
assert_eq!(reported.len(), 2, "one per transport: {reported:?}");
assert!(
reported.iter().any(|addr| addr.contains("/quic-v1")),
"{reported:?}"
);
assert!(
reported.iter().any(|addr| addr.contains("/tcp/")),
"{reported:?}"
);
}
#[cfg(feature = "tcp")]
#[test]
fn a_second_tcp_address_joins_the_transport_that_already_serves_tcp() {
let mut endpoint = Endpoint::builder()
.tcp("127.0.0.1:0")
.tcp("127.0.0.1:0")
.bind()
.expect("both tcp addresses bind");
let addrs = endpoint.listen_all().expect("listen");
assert_eq!(addrs.len(), 2, "both addresses are listening: {addrs:?}");
assert_ne!(
tcp_port(&addrs[0]),
tcp_port(&addrs[1]),
"two binds, two sockets"
);
let second = addrs[1].clone();
let _driver = Driven::new(endpoint);
let mut dialer = Endpoint::builder()
.bind_tcp("127.0.0.1:0")
.expect("bind dialer");
dialer.dial(&second).expect("dial the second address");
wait_for(
&mut dialer,
"connection",
|event| matches!(event, Event::ConnectionEstablished { peer_id, .. } if peer_id == second.peer_id()),
);
}
#[cfg(feature = "tcp")]
#[test]
fn listening_arms_every_bound_transport_not_just_the_first() {
let mut listener = Endpoint::builder()
.tcp("127.0.0.1:0")
.quic("127.0.0.1:0")
.bind()
.expect("bind both");
let bound = listener
.swarm()
.transport()
.local_addresses()
.into_iter()
.find(|addr| addr.to_string().contains("quic"))
.expect("a bound quic socket");
let quic_addr = PeerAddr::new(bound, listener.peer_id().clone()).expect("target");
let first = listener.listen().expect("listen returns the first address");
assert!(
first.transport().to_string().contains("/tcp/"),
"the returned address is the first bound one: {first:?}"
);
let _driver = Driven::new(listener);
let mut dialer = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind dialer");
dialer.dial(&quic_addr).expect("dial");
wait_for(
&mut dialer,
"connection",
|event| matches!(event, Event::ConnectionEstablished { peer_id, .. } if peer_id == quic_addr.peer_id()),
);
}
#[cfg(feature = "tcp")]
#[test]
fn a_peer_is_reached_over_the_transport_its_address_names() {
let mut over_tcp = Endpoint::builder()
.bind_tcp("127.0.0.1:0")
.expect("bind tcp peer");
let mut over_quic = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind quic peer");
let tcp_addr = over_tcp.listen().expect("tcp peer listens");
let quic_addr = over_quic.listen().expect("quic peer listens");
let mut dialer = Endpoint::builder()
.quic("127.0.0.1:0")
.tcp("127.0.0.1:0")
.bind()
.expect("bind dialer");
let _drivers = (Driven::new(over_tcp), Driven::new(over_quic));
for (addr, expected) in [
(&tcp_addr, ConnectionNamespace::TCP_IPV4),
(&quic_addr, ConnectionNamespace::QUIC_IPV4),
] {
dialer.dial(addr).expect("dial");
let event = wait_for(
&mut dialer,
"connection",
|event| matches!(event, Event::ConnectionEstablished { peer_id, .. } if peer_id == addr.peer_id()),
);
let Event::ConnectionEstablished { conn_id, .. } = event else {
unreachable!("the predicate matched a connection")
};
assert_eq!(
conn_id.namespace(),
expected,
"{} should have been carried by {expected}",
addr.transport()
);
}
}
#[cfg(feature = "tcp")]
#[test]
fn tcp_limits_and_namespace_reach_the_transport() {
let mut endpoint = Endpoint::builder()
.tcp_config(TcpConfig {
namespace: ConnectionNamespace::TCP_IPV6,
..TcpConfig::default()
})
.bind_tcp("127.0.0.1:0")
.expect("bind tcp endpoint");
let target = PeerAddr::new(
"/ip4/127.0.0.1/tcp/1".parse().expect("address"),
Ed25519Keypair::generate().peer_id(),
)
.expect("target");
let ids = endpoint.dial(&target).expect("the dial starts");
assert_eq!(ids.len(), 1);
assert_eq!(ids[0].namespace(), ConnectionNamespace::TCP_IPV6);
}
#[cfg(feature = "tcp")]
#[test]
fn a_tcp_transport_tagged_for_another_carrier_is_refused() {
let Err(error) = Endpoint::builder()
.tcp_config(TcpConfig {
namespace: ConnectionNamespace::QUIC_IPV4,
..TcpConfig::default()
})
.bind_tcp("127.0.0.1:0")
else {
panic!("a tcp transport must not claim another transport's ids");
};
assert!(
format!("{error}").contains("must allocate in a tcp namespace"),
"got {error}"
);
}
#[cfg(feature = "tcp")]
#[test]
fn a_bind_spec_keeps_every_address_it_named() {
let resolved: Vec<std::net::SocketAddr> = vec![
"127.0.0.1:4001".parse().expect("v4"),
"[::1]:4001".parse().expect("v6"),
"127.0.0.1:4001".parse().expect("v4 again"),
];
assert_eq!(
tcp_addrs_of(resolved)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
vec!["/ip4/127.0.0.1/tcp/4001", "/ip6/::1/tcp/4001"]
);
}
#[cfg(feature = "tcp")]
#[test]
fn a_tcp_multiaddr_binds_and_a_wrong_shape_is_refused() {
let mut endpoint = Endpoint::builder()
.tcp_multiaddr(&"/ip4/127.0.0.1/tcp/0".parse().expect("address"))
.bind()
.expect("bind by multiaddr");
let addrs = endpoint.listen_all().expect("listen");
assert_eq!(
addrs[0].transport().to_string(),
format!("/ip4/127.0.0.1/tcp/{}", tcp_port(&addrs[0]))
);
let Err(error) = Endpoint::builder()
.tcp_multiaddr(&"/ip4/127.0.0.1/udp/0/quic-v1".parse().expect("address"))
.bind()
else {
panic!("a /udp address is not a tcp bind address");
};
assert!(
matches!(&error, Error::Transport(TransportError::InvalidAddress { reason, .. })
if reason.contains("not a /tcp transport address")),
"got {error:?}"
);
}
#[test]
fn an_address_no_bound_transport_serves_is_refused_by_name() {
let mut endpoint = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind loopback endpoint");
let target = PeerAddr::new(
"/ip4/127.0.0.1/tcp/4001".parse().expect("address"),
Ed25519Keypair::generate().peer_id(),
)
.expect("target");
let error = endpoint
.dial(&target)
.expect_err("nothing serves /tcp here");
assert!(
format!("{error}").contains("Tcp"),
"the error should name the missing transport, got {error}"
);
}
#[test]
fn a_dial_reaches_a_peer_on_the_transport_its_address_names() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let mut listener = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind listener");
let listen_addr = listener.listen().expect("listen");
let mut dialer = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind dialer");
let stop = Arc::new(AtomicBool::new(false));
let listener_stop = Arc::clone(&stop);
let listener_thread = std::thread::spawn(move || {
while !listener_stop.load(Ordering::Relaxed) {
listener
.next_event(Duration::from_millis(20))
.expect("drive listener");
}
});
let ids = dialer.dial(&listen_addr).expect("dial");
assert_eq!(ids.len(), 1, "one address, one dial: {ids:?}");
let connected = dialer
.next_event(Duration::from_secs(5))
.expect("drive dialer");
stop.store(true, Ordering::Relaxed);
listener_thread.join().expect("listener driver exits");
assert!(
matches!(&connected, Some(Event::ConnectionEstablished { peer_id, .. })
if peer_id == listen_addr.peer_id()),
"got {connected:?}"
);
}
#[test]
fn next_wake_reports_deadline_without_active_drivers() {
let mut endpoint = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind loopback endpoint");
assert!(matches!(
endpoint.next_wake(Duration::ZERO).expect("poll endpoint"),
EndpointWake::Deadline
));
}
#[test]
fn next_wake_reports_interrupt_without_active_drivers() {
let mut endpoint = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind loopback endpoint");
endpoint.wait_handle().interrupt();
assert!(matches!(
endpoint.next_wake(Deadline::NEVER).expect("poll endpoint"),
EndpointWake::Interrupted
));
}
#[cfg(feature = "nat")]
#[test]
fn next_wake_transfers_buffered_application_event_ownership() {
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig::default())
.bind_quic("127.0.0.1:0")
.expect("bind NAT endpoint");
let peer_id = Ed25519Keypair::generate().peer_id();
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: peer_id.clone(),
conn_id: ConnectionId::new(7),
});
assert!(matches!(
endpoint.next_wake(Deadline::NEVER).expect("wake"),
EndpointWake::Event(Event::ConnectionClosed {
peer_id: returned,
..
}) if returned == peer_id
));
assert!(endpoint.pending_events.is_empty());
}
#[cfg(feature = "nat")]
#[test]
fn next_wake_reports_already_queued_driver_progress_without_consuming_it() {
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig::default())
.bind_quic("127.0.0.1:0")
.expect("bind NAT endpoint");
endpoint
.connect(&Ed25519Keypair::generate().peer_id())
.expect("start endpoint-local connect");
assert!(matches!(
endpoint.next_wake(Deadline::NEVER).expect("wake"),
EndpointWake::DriverProgress
));
assert_eq!(endpoint.take_nat_events().len(), 1);
}
#[cfg(feature = "pubsub")]
#[test]
fn next_wake_reports_queued_pubsub_progress_without_consuming_it() {
let mut endpoint = Endpoint::builder()
.pubsub()
.bind_quic("127.0.0.1:0")
.expect("bind pubsub endpoint");
let peer = Ed25519Keypair::generate().peer_id();
endpoint
.pubsub
.as_mut()
.expect("pubsub configured")
.events
.push_back(PubsubEvent::PeerSubscribed {
peer: peer.clone(),
topic: "test".into(),
});
assert!(matches!(
endpoint.next_wake(Deadline::NEVER).expect("wake"),
EndpointWake::DriverProgress
));
assert!(matches!(
endpoint.take_pubsub_events().as_slice(),
[PubsubEvent::PeerSubscribed {
peer: returned,
topic
}] if returned == &peer && topic == "test"
));
}
#[cfg(feature = "nat")]
#[test]
fn next_wake_honors_expired_deadline_with_active_driver() {
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig::default())
.bind_quic("127.0.0.1:0")
.expect("bind NAT endpoint");
assert!(matches!(
endpoint.next_wake(Duration::ZERO).expect("expired wake"),
EndpointWake::Deadline
));
}
#[cfg(feature = "nat")]
#[test]
fn next_wake_returns_application_event_produced_during_driver_poll() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig::default())
.bind_quic("127.0.0.1:0")
.expect("bind driven endpoint");
let mut remote = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind remote endpoint");
endpoint.listen().expect("driven endpoint listens");
let remote_addr = remote.listen().expect("remote listens");
let stop = Arc::new(AtomicBool::new(false));
let remote_stop = Arc::clone(&stop);
let remote_thread = std::thread::spawn(move || {
while !remote_stop.load(Ordering::Relaxed) {
remote
.next_event(Duration::from_millis(20))
.expect("drive remote");
}
});
endpoint.dial(&remote_addr).expect("dial remote");
let wake = endpoint
.next_wake(Duration::from_secs(5))
.expect("wait for application event");
stop.store(true, Ordering::Relaxed);
remote_thread.join().expect("remote driver exits");
assert!(matches!(
wake,
EndpointWake::Event(Event::ConnectionEstablished { peer_id, .. })
if peer_id == *remote_addr.peer_id()
));
}
#[cfg(feature = "nat")]
#[test]
fn next_wake_returns_driver_progress_produced_by_timer_during_poll() {
use std::net::{IpAddr, Ipv4Addr};
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig {
connect_deadline_ms: 20,
..NatConfig::default()
})
.bind_quic("127.0.0.1:0")
.expect("bind NAT endpoint");
let unreachable = PeerAddr::quic_v1(
IpAddr::V4(Ipv4Addr::LOCALHOST),
9,
Ed25519Keypair::generate().peer_id(),
);
endpoint
.connect_addr(&unreachable)
.expect("start timed connect");
assert!(endpoint.take_nat_events().is_empty());
assert!(matches!(
endpoint
.next_wake(Duration::from_secs(1))
.expect("wait for connect deadline"),
EndpointWake::DriverProgress
));
assert!(matches!(
endpoint.take_nat_events().as_slice(),
[NatEvent::ConnectFailed { .. }]
));
}
#[cfg(any(feature = "discovery", feature = "mdns"))]
#[test]
fn discovery_clock_is_present_only_for_an_active_source() {
let inactive = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind plain endpoint");
assert_eq!(inactive.discovery_now_ms(), None);
let builder = Endpoint::builder();
#[cfg(feature = "discovery")]
let builder = builder.discovery();
#[cfg(all(feature = "mdns", not(feature = "discovery")))]
let builder = builder.mdns();
let active = builder
.bind_quic("127.0.0.1:0")
.expect("bind discovery endpoint");
let first = active.discovery_now_ms().expect("discovery clock");
let second = active.discovery_now_ms().expect("discovery clock");
assert!(second >= first);
}
#[cfg(feature = "mdns")]
#[test]
fn mdns_config_is_rejected_before_binding() {
let config = MdnsConfig {
max_packet_bytes: 4_097,
..MdnsConfig::default()
};
assert!(matches!(
Endpoint::builder().mdns_config(config),
Err(MdnsConfigError::InvalidMaxPacketBytes)
));
}
#[cfg(feature = "mdns")]
#[test]
fn mdns_shutdown_is_idempotent_and_leaves_quic_usable() {
let mut endpoint = Endpoint::builder()
.mdns()
.peer_discovery_config(PeerDiscoveryConfig {
auto_dial: false,
..PeerDiscoveryConfig::default()
})
.expect("valid peer discovery policy")
.bind_quic("127.0.0.1:0")
.expect("bind mDNS endpoint");
endpoint.listen().expect("QUIC listens");
endpoint.shutdown().expect("first mDNS shutdown");
endpoint.shutdown().expect("second mDNS shutdown");
assert!(
endpoint.poll().is_ok(),
"QUIC remains usable after shutdown"
);
}
#[cfg(feature = "discovery")]
#[test]
fn discovery_topic_cannot_be_unsubscribed_independently() {
let topic = "/minip2p/test/discovery";
let config = BeaconConfig {
topic: topic.into(),
..BeaconConfig::default()
};
let mut endpoint = Endpoint::builder()
.discovery_config(config)
.expect("valid discovery configuration")
.bind_quic("127.0.0.1:0")
.expect("bind discovery endpoint");
assert!(matches!(
endpoint.unsubscribe(topic),
Err(PubsubError::DiscoveryTopicReserved)
));
}
#[cfg(feature = "discovery")]
#[test]
fn discovery_focused_waits_preserve_events_and_enforce_the_spin_guard() {
let mut endpoint = Endpoint::builder()
.discovery()
.bind_quic("127.0.0.1:0")
.expect("bind discovery endpoint");
let unrelated = Ed25519Keypair::generate().peer_id();
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
assert!(
endpoint
.next_discovery_event(Duration::from_millis(5))
.expect("discovery wait")
.is_none(),
"a buffered application event must not make next_discovery_event spin"
);
assert!(matches!(
endpoint
.next_event(Duration::from_millis(1))
.expect("drain buffered event"),
Some(Event::ConnectionClosed { peer_id, .. }) if peer_id == unrelated
));
for _ in 0..RUN_UNTIL_SKIP_LIMIT {
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
}
assert!(matches!(
endpoint.next_discovery_event(Deadline::NEVER),
Err(DiscoveryError::Driver(Error::EventBacklogExceeded { limit }))
if limit == RUN_UNTIL_SKIP_LIMIT
));
}
use std::time::Duration;
const PROTOCOL: &str = "/myapp/1.0.0";
#[test]
fn builder_protocol_registers_for_stream_routing() {
let mut endpoint = Endpoint::builder()
.protocol(PROTOCOL)
.bind_quic("127.0.0.1:0")
.expect("bind loopback endpoint");
let peer_id = Ed25519Keypair::generate().peer_id();
assert!(matches!(
endpoint.open_stream(&peer_id, PROTOCOL),
Err(Error::Swarm(SwarmError::NotConnected { .. }))
));
assert!(matches!(
endpoint.open_stream(&peer_id, "/other/1.0.0"),
Err(Error::Swarm(SwarmError::ProtocolNotRegistered { .. }))
));
}
#[test]
fn builder_rejects_reserved_protocol_ids() {
for reserved in RESERVED_PROTOCOL_IDS {
let error = Endpoint::builder()
.protocol(reserved)
.bind_quic("127.0.0.1:0")
.err()
.expect("reserved ids must fail the build");
assert!(matches!(
error,
Error::Swarm(SwarmError::ReservedProtocol { .. })
));
}
}
#[test]
fn builder_rejects_reserved_protocol_ids_before_binding() {
let error = Endpoint::builder()
.protocol(RESERVED_PROTOCOL_IDS[0])
.bind_quic("not-a-bindable-address")
.err()
.expect("reserved ids must fail the build");
assert!(matches!(
error,
Error::Swarm(SwarmError::ReservedProtocol { .. })
));
}
#[test]
fn add_protocol_rejects_reserved_protocol_ids() {
let mut endpoint = Endpoint::builder()
.bind_quic("127.0.0.1:0")
.expect("bind loopback endpoint");
let error = endpoint
.add_protocol(RESERVED_PROTOCOL_IDS[0])
.expect_err("reserved ids must be rejected");
assert!(matches!(
error,
Error::Swarm(SwarmError::ReservedProtocol { .. })
));
endpoint
.add_protocol(PROTOCOL)
.expect("application ids must be accepted");
}
#[cfg(feature = "nat")]
#[test]
fn nat_focused_waits_do_not_repoll_buffered_application_events() {
let mut endpoint = Endpoint::builder()
.nat_config(NatConfig::default())
.bind_quic("127.0.0.1:0")
.expect("bind endpoint");
let unrelated = Ed25519Keypair::generate().peer_id();
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
assert!(
endpoint
.next_nat_event(Duration::from_millis(5))
.expect("NAT wait")
.is_none(),
"a buffered application event must not make next_nat_event spin"
);
assert!(matches!(
endpoint
.next_event(Duration::from_millis(1))
.expect("drain buffered event"),
Some(Event::ConnectionClosed { peer_id, .. }) if peer_id == unrelated
));
let id = endpoint
.connect(&Ed25519Keypair::generate().peer_id())
.expect("connect");
endpoint
.nat
.as_mut()
.expect("NAT configured")
.events
.clear();
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
assert!(
endpoint
.wait_path(id, Duration::from_millis(5))
.expect("path wait")
.is_none(),
"a buffered application event must not make wait_path spin"
);
assert!(matches!(
endpoint
.next_event(Duration::from_millis(1))
.expect("drain buffered event"),
Some(Event::ConnectionClosed { peer_id, .. }) if peer_id == unrelated
));
for _ in 0..RUN_UNTIL_SKIP_LIMIT {
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
}
assert!(matches!(
endpoint.next_nat_event(Deadline::NEVER),
Err(Error::EventBacklogExceeded { limit }) if limit == RUN_UNTIL_SKIP_LIMIT
));
}
#[cfg(feature = "pubsub")]
#[test]
fn pubsub_focused_waits_do_not_repoll_buffered_application_events() {
let mut endpoint = Endpoint::builder()
.pubsub()
.bind_quic("127.0.0.1:0")
.expect("bind endpoint");
let unrelated = Ed25519Keypair::generate().peer_id();
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
assert!(
endpoint
.next_pubsub_event(Duration::ZERO)
.expect("pubsub wait")
.is_none(),
"a buffered application event must not make next_pubsub_event spin"
);
assert!(matches!(
endpoint
.next_event(Duration::ZERO)
.expect("drain buffered event"),
Some(Event::ConnectionClosed { peer_id, .. }) if peer_id == unrelated
));
for _ in 0..RUN_UNTIL_SKIP_LIMIT {
endpoint.pending_events.push_back(Event::ConnectionClosed {
peer_id: unrelated.clone(),
conn_id: ConnectionId::new(1),
});
}
assert!(matches!(
endpoint.next_pubsub_event(Deadline::NEVER),
Err(PubsubError::Driver(Error::EventBacklogExceeded { limit }))
if limit == RUN_UNTIL_SKIP_LIMIT
));
}
}