use std::collections::HashMap;
use crate::caps::{
PEX_MAX_ADDED, PEX_MAX_DROPPED, PEX_MAX_HINTS, PEX_MAX_INTERVAL, PEX_MAX_RECEIVED_PER_LINK,
PEX_MAX_SNAPSHOT, PEX_VERSION, PEX_VIOLATION_LIMIT,
};
use crate::entry::{PeerEntry, ValidateCtx};
use crate::error::PexErrorCode;
use crate::state::{LinkState, RecvPhase};
use crate::timer::{arrival_floor_ms, clamp_interval, effective_interval_secs, jitter_ms};
use crate::wire::PexMessage;
#[derive(Debug, Clone)]
pub struct PexConfig {
pub local_peer_id: String,
pub network_id: String,
pub flags: Vec<String>,
pub interval: u32,
pub jitter: bool,
}
impl PexConfig {
#[must_use]
pub fn new(local_peer_id: impl Into<String>, network_id: impl Into<String>) -> Self {
PexConfig {
local_peer_id: local_peer_id.into(),
network_id: network_id.into(),
flags: Vec::new(),
interval: crate::caps::PEX_DEFAULT_INTERVAL,
jitter: true,
}
}
#[must_use]
pub fn with_flags(mut self, flags: Vec<String>) -> Self {
self.flags = flags;
self
}
#[must_use]
pub fn with_interval(mut self, secs: u32) -> Self {
self.interval = clamp_interval(secs);
self
}
#[must_use]
pub fn with_jitter(mut self, jitter: bool) -> Self {
self.jitter = jitter;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PexEvent {
Candidates(Vec<PeerEntry>),
Dropped {
peer_ids: Vec<String>,
},
Violation {
code: u16,
mute: bool,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PexOutcome {
pub replies: Vec<PexMessage>,
pub events: Vec<PexEvent>,
}
#[derive(Debug, Clone)]
struct ReceivedHint {
source: String,
last_seen: u64,
}
#[derive(Debug)]
pub struct PexEngine {
cfg: PexConfig,
known: HashMap<String, PeerEntry>,
links: HashMap<String, LinkState>,
hints: HashMap<String, ReceivedHint>,
known_epoch: u64,
advertisable_cache: std::cell::RefCell<Option<(u64, u64, Vec<PeerEntry>)>>,
#[cfg(test)]
advertisable_rebuilds: std::cell::Cell<u64>,
}
impl PexEngine {
#[must_use]
pub fn new(cfg: PexConfig) -> Self {
PexEngine {
cfg,
known: HashMap::new(),
links: HashMap::new(),
hints: HashMap::new(),
known_epoch: 0,
advertisable_cache: std::cell::RefCell::new(None),
#[cfg(test)]
advertisable_rebuilds: std::cell::Cell::new(0),
}
}
pub fn upsert_known(&mut self, entry: PeerEntry) {
if entry.peer_id == self.cfg.local_peer_id {
return;
}
self.known.insert(entry.peer_id.clone(), entry);
self.known_epoch = self.known_epoch.wrapping_add(1);
}
pub fn remove_known(&mut self, peer_id: &str) {
self.known.remove(peer_id);
self.known_epoch = self.known_epoch.wrapping_add(1);
}
pub fn link_up(&mut self, peer_id: &str, now_ms: u64) -> Vec<PexMessage> {
let interval = self.cfg.interval;
let handshake = PexMessage::PexHandshake {
version: PEX_VERSION,
network_id: self.cfg.network_id.clone(),
interval,
flags: self.cfg.flags.clone(),
};
let now_secs = now_ms / 1000;
let mut peers = self.advertisable_for(peer_id, now_secs);
peers.truncate(PEX_MAX_SNAPSHOT);
let remote_declared = self.links.get(peer_id).and_then(|l| l.remote_declared_secs);
let jitter = self.draw_jitter(effective_interval_secs(interval, remote_declared));
let link = self
.links
.entry(peer_id.to_string())
.or_insert_with(|| LinkState::new(interval));
link.self_interval_secs = interval;
link.handshake_sent = true;
for e in &peers {
link.told.insert(e.peer_id.clone(), e.fingerprint_hash());
}
link.snapshot_sent = true;
link.last_data_send_ms = Some(now_ms);
link.send_jitter_ms = jitter;
vec![handshake, PexMessage::PexSnapshot { peers }]
}
pub fn link_down(&mut self, peer_id: &str) {
self.links.remove(peer_id);
self.hints.retain(|_, h| h.source != peer_id);
}
pub fn on_message(&mut self, peer_id: &str, msg: PexMessage, now_ms: u64) -> PexOutcome {
let interval = self.cfg.interval;
let muted = {
let link = self
.links
.entry(peer_id.to_string())
.or_insert_with(|| LinkState::new(interval));
link.muted
};
if muted {
return PexOutcome::default();
}
match msg {
PexMessage::PexError { code, .. } => self.on_pex_error(peer_id, code, now_ms),
PexMessage::PexHandshake {
version,
network_id,
interval: declared,
..
} => self.on_handshake(peer_id, version, &network_id, declared),
PexMessage::PexSnapshot { peers } => self.on_snapshot(peer_id, peers, now_ms),
PexMessage::PexDelta { added, dropped } => {
self.on_delta(peer_id, added, dropped, now_ms)
}
}
}
pub fn record_violation(
&mut self,
peer_id: &str,
code: PexErrorCode,
_now_ms: u64,
) -> PexOutcome {
let interval = self.cfg.interval;
self.links
.entry(peer_id.to_string())
.or_insert_with(|| LinkState::new(interval));
self.strike(peer_id, code)
}
pub fn tick(&mut self, now_ms: u64) -> Vec<(String, PexMessage)> {
let now_secs = now_ms / 1000;
let mut out = Vec::new();
let peer_ids: Vec<String> = self.links.keys().cloned().collect();
for peer_id in peer_ids {
let (eligible, effective) = {
let link = &self.links[&peer_id];
if !link.snapshot_sent {
continue; }
let effective =
effective_interval_secs(link.self_interval_secs, link.remote_declared_secs);
let base = link.last_data_send_ms.unwrap_or(0);
let eligible = now_ms >= base + u64::from(effective) * 1000 + link.send_jitter_ms;
(eligible, effective)
};
if !eligible {
continue;
}
let (added, dropped) = self.build_delta(&peer_id, now_secs);
if added.is_empty() && dropped.is_empty() {
continue; }
let link = self.links.get_mut(&peer_id).expect("link exists");
for e in &added {
link.told.insert(e.peer_id.clone(), e.fingerprint_hash());
}
for id in &dropped {
link.told.remove(id);
}
link.last_data_send_ms = Some(now_ms);
let jitter = self.draw_jitter(effective);
self.links
.get_mut(&peer_id)
.expect("link exists")
.send_jitter_ms = jitter;
out.push((peer_id, PexMessage::PexDelta { added, dropped }));
}
out
}
#[must_use]
pub fn known_count(&self) -> usize {
self.known.len()
}
#[must_use]
pub fn link_count(&self) -> usize {
self.links.len()
}
#[must_use]
pub fn is_muted(&self, peer_id: &str) -> bool {
self.links.get(peer_id).is_some_and(|l| l.muted)
}
#[must_use]
pub fn strikes(&self, peer_id: &str) -> u32 {
self.links.get(peer_id).map_or(0, |l| l.strikes)
}
#[must_use]
pub fn told_count(&self, peer_id: &str) -> usize {
self.links.get(peer_id).map_or(0, |l| l.told.len())
}
#[must_use]
pub fn current_hint(&self, peer_id: &str) -> Option<(&str, u64)> {
self.hints
.get(peer_id)
.map(|h| (h.source.as_str(), h.last_seen))
}
#[must_use]
pub fn received_count(&self, peer_id: &str) -> usize {
self.links.get(peer_id).map_or(0, |l| l.received.len())
}
#[must_use]
pub fn hints_count(&self) -> usize {
self.hints.len()
}
fn draw_jitter(&self, effective_secs: u32) -> u64 {
if self.cfg.jitter {
jitter_ms(effective_secs)
} else {
0
}
}
fn on_pex_error(&mut self, peer_id: &str, code: u16, now_ms: u64) -> PexOutcome {
if code == PexErrorCode::RateViolation.as_u16() {
if let Some(link) = self.links.get_mut(peer_id) {
let plausible = link.last_data_send_ms.is_some_and(|sent| {
now_ms.saturating_sub(sent) < arrival_floor_ms(link.self_interval_secs)
});
let effective_ms = u64::from(link.self_interval_secs) * 1000;
let rate_limited = link
.last_backoff_applied_ms
.is_some_and(|applied| now_ms.saturating_sub(applied) < effective_ms);
if plausible && !rate_limited {
link.self_interval_secs = clamp_interval(
(link.self_interval_secs.saturating_mul(2)).min(PEX_MAX_INTERVAL),
);
link.last_backoff_applied_ms = Some(now_ms);
}
}
}
PexOutcome::default()
}
fn on_handshake(
&mut self,
peer_id: &str,
version: u32,
network_id: &str,
declared: u32,
) -> PexOutcome {
let phase = self.links[peer_id].phase;
if phase != RecvPhase::AwaitingHandshake {
return self.strike(peer_id, PexErrorCode::ProtocolViolation);
}
if version != PEX_VERSION {
return self.mute_mismatch(peer_id, PexErrorCode::UnsupportedVersion);
}
if network_id != self.cfg.network_id {
return self.mute_mismatch(peer_id, PexErrorCode::NetworkMismatch);
}
let link = self.links.get_mut(peer_id).expect("link exists");
link.remote_declared_secs = Some(clamp_interval(declared));
link.phase = RecvPhase::AwaitingSnapshot;
PexOutcome::default()
}
fn on_snapshot(&mut self, peer_id: &str, peers: Vec<PeerEntry>, now_ms: u64) -> PexOutcome {
match self.links[peer_id].phase {
RecvPhase::AwaitingHandshake => self.strike(peer_id, PexErrorCode::ProtocolViolation),
RecvPhase::Streaming => self.strike(peer_id, PexErrorCode::ProtocolViolation),
RecvPhase::AwaitingSnapshot => {
if peers.len() > PEX_MAX_SNAPSHOT {
return self.strike(peer_id, PexErrorCode::Oversized);
}
let link = self.links.get_mut(peer_id).expect("link exists");
link.phase = RecvPhase::Streaming;
link.last_arrival_ms = Some(now_ms); self.ingest_added(peer_id, peers, now_ms)
}
}
}
fn on_delta(
&mut self,
peer_id: &str,
added: Vec<PeerEntry>,
dropped: Vec<String>,
now_ms: u64,
) -> PexOutcome {
match self.links[peer_id].phase {
RecvPhase::AwaitingHandshake | RecvPhase::AwaitingSnapshot => {
return self.strike(peer_id, PexErrorCode::ProtocolViolation);
}
RecvPhase::Streaming => {}
}
let (floor, last) = {
let link = &self.links[peer_id];
(
arrival_floor_ms(link.remote_declared_secs.unwrap_or(0)),
link.last_arrival_ms,
)
};
if let Some(last) = last {
if now_ms.saturating_sub(last) < floor {
return self.strike(peer_id, PexErrorCode::RateViolation);
}
}
if added.len() > PEX_MAX_ADDED || dropped.len() > PEX_MAX_DROPPED {
return self.strike(peer_id, PexErrorCode::Oversized);
}
let added_ids: std::collections::HashSet<&str> =
added.iter().map(|e| e.peer_id.as_str()).collect();
if dropped.iter().any(|d| added_ids.contains(d.as_str())) {
return self.strike(peer_id, PexErrorCode::BadMessage);
}
self.links
.get_mut(peer_id)
.expect("link exists")
.last_arrival_ms = Some(now_ms);
let mut outcome = self.ingest_added(peer_id, added, now_ms);
outcome
.events
.extend(self.ingest_dropped(peer_id, dropped).events);
outcome
}
fn ingest_added(&mut self, peer_id: &str, entries: Vec<PeerEntry>, now_ms: u64) -> PexOutcome {
let now_secs = now_ms / 1000;
let mut candidates = Vec::new();
for e in entries {
let ctx = ValidateCtx {
receiver_peer_id: &self.cfg.local_peer_id,
sender_peer_id: peer_id,
network_id: &self.cfg.network_id,
now_secs,
};
if e.validate(&ctx).is_err() {
continue; }
let ce = e.clamped(now_secs);
let link = self.links.get_mut(peer_id).expect("link exists");
if !link.received.contains_key(&ce.peer_id)
&& link.received.len() >= PEX_MAX_RECEIVED_PER_LINK
{
evict_oldest(&mut link.received, |last_seen| *last_seen);
}
link.received.insert(ce.peer_id.clone(), ce.last_seen);
let fresher = match self.hints.get(&ce.peer_id) {
Some(h) => ce.last_seen > h.last_seen,
None => true,
};
if fresher {
if !self.hints.contains_key(&ce.peer_id) && self.hints.len() >= PEX_MAX_HINTS {
evict_oldest(&mut self.hints, |h| h.last_seen);
}
self.hints.insert(
ce.peer_id.clone(),
ReceivedHint {
source: peer_id.to_string(),
last_seen: ce.last_seen,
},
);
candidates.push(ce);
}
}
let mut outcome = PexOutcome::default();
if !candidates.is_empty() {
outcome.events.push(PexEvent::Candidates(candidates));
}
outcome
}
fn ingest_dropped(&mut self, peer_id: &str, dropped: Vec<String>) -> PexOutcome {
let mut attributed = Vec::new();
for id in dropped {
let told_us = self
.links
.get_mut(peer_id)
.expect("link exists")
.received
.remove(&id)
.is_some();
if told_us {
if let Some(h) = self.hints.get(&id) {
if h.source == peer_id {
self.hints.remove(&id);
}
}
attributed.push(id);
}
}
let mut outcome = PexOutcome::default();
if !attributed.is_empty() {
outcome.events.push(PexEvent::Dropped {
peer_ids: attributed,
});
}
outcome
}
fn strike(&mut self, peer_id: &str, code: PexErrorCode) -> PexOutcome {
let link = self.links.get_mut(peer_id).expect("link exists");
link.strikes += 1;
let mute = link.strikes >= PEX_VIOLATION_LIMIT;
if mute {
link.muted = true;
self.free_muted_link_state(peer_id);
}
PexOutcome {
replies: vec![PexMessage::PexError {
code: code.as_u16(),
message: code.message().to_string(),
}],
events: vec![PexEvent::Violation {
code: code.as_u16(),
mute,
}],
}
}
fn mute_mismatch(&mut self, peer_id: &str, code: PexErrorCode) -> PexOutcome {
self.links.get_mut(peer_id).expect("link exists").muted = true;
self.free_muted_link_state(peer_id);
PexOutcome {
replies: vec![PexMessage::PexError {
code: code.as_u16(),
message: code.message().to_string(),
}],
events: vec![PexEvent::Violation {
code: code.as_u16(),
mute: true,
}],
}
}
fn free_muted_link_state(&mut self, peer_id: &str) {
if let Some(link) = self.links.get_mut(peer_id) {
link.received.clear();
}
self.hints.retain(|_, h| h.source != peer_id);
}
}
fn evict_oldest<V>(map: &mut HashMap<String, V>, last_seen: impl Fn(&V) -> u64) {
if let Some(oldest_key) = map
.iter()
.min_by(|(ka, va), (kb, vb)| last_seen(va).cmp(&last_seen(vb)).then_with(|| ka.cmp(kb)))
.map(|(k, _)| k.clone())
{
map.remove(&oldest_key);
}
}
fn advertisable_base(
known: &HashMap<String, PeerEntry>,
local_peer_id: &str,
now_secs: u64,
) -> Vec<PeerEntry> {
let mut out: Vec<PeerEntry> = known
.values()
.filter(|e| e.peer_id != local_peer_id)
.filter(|e| {
e.last_seen >= now_secs || now_secs - e.last_seen <= crate::caps::PEX_MAX_ENTRY_AGE
})
.cloned()
.collect();
out.sort_by(|a, b| {
b.last_seen
.cmp(&a.last_seen)
.then_with(|| a.peer_id.cmp(&b.peer_id))
});
out
}
impl PexEngine {
fn advertisable_cached(&self, now_secs: u64) -> std::cell::Ref<'_, Vec<PeerEntry>> {
{
let cache = self.advertisable_cache.borrow();
if let Some((epoch, cached_secs, _)) = cache.as_ref() {
if *epoch == self.known_epoch && *cached_secs == now_secs {
drop(cache);
return std::cell::Ref::map(self.advertisable_cache.borrow(), |c| {
&c.as_ref().unwrap().2
});
}
}
}
let fresh = advertisable_base(&self.known, &self.cfg.local_peer_id, now_secs);
#[cfg(test)]
self.advertisable_rebuilds
.set(self.advertisable_rebuilds.get() + 1);
*self.advertisable_cache.borrow_mut() = Some((self.known_epoch, now_secs, fresh));
std::cell::Ref::map(self.advertisable_cache.borrow(), |c| &c.as_ref().unwrap().2)
}
#[cfg(test)]
fn advertisable_rebuild_count(&self) -> u64 {
self.advertisable_rebuilds.get()
}
fn advertisable_for(&self, partner: &str, now_secs: u64) -> Vec<PeerEntry> {
self.advertisable_cached(now_secs)
.iter()
.filter(|e| e.peer_id != partner)
.cloned()
.collect()
}
fn build_delta(&self, peer_id: &str, now_secs: u64) -> (Vec<PeerEntry>, Vec<String>) {
let link = &self.links[peer_id];
let base = self.advertisable_cached(now_secs);
let mut added = Vec::new();
let mut advert_ids: std::collections::HashSet<&str> = std::collections::HashSet::new();
for e in base.iter().filter(|e| e.peer_id != peer_id) {
advert_ids.insert(e.peer_id.as_str());
if added.len() >= PEX_MAX_ADDED {
continue; }
match link.told.get(&e.peer_id) {
Some(fp) if *fp == e.fingerprint_hash() => {} _ => added.push(e.clone()),
}
}
let mut dropped = Vec::new();
for id in link.told.keys() {
if dropped.len() >= PEX_MAX_DROPPED {
break;
}
if !advert_ids.contains(id.as_str()) {
dropped.push(id.clone());
}
}
dropped.sort();
(added, dropped)
}
}
#[cfg(test)]
mod cap_tests {
use super::*;
use crate::caps::{PEX_MAX_HINTS, PEX_MAX_RECEIVED_PER_LINK};
use crate::entry::{Address, Provenance};
fn hex_id(n: u32) -> String {
format!("{n:064x}")
}
fn eng(local: &str) -> PexEngine {
PexEngine::new(PexConfig::new(local.to_string(), "mainnet".to_string()).with_jitter(false))
}
fn ensure_link(e: &mut PexEngine, peer_id: &str) {
let interval = e.cfg.interval;
e.links
.entry(peer_id.to_string())
.or_insert_with(|| LinkState::new(interval));
}
fn distinct_entry(n: u32, now_secs: u64) -> PeerEntry {
PeerEntry::new(hex_id(n), "mainnet", now_secs, Provenance::Direct)
.with_address(Address::direct("203.0.113.7", 9444))
}
#[test]
fn received_map_is_capped_per_link_with_eviction() {
let local = hex_id(0);
let sender = hex_id(1);
let mut e = eng(&local);
let now_ms = 1_000_000_000_u64;
ensure_link(&mut e, &sender);
let total = PEX_MAX_RECEIVED_PER_LINK + 500;
for n in 2..2 + total as u32 {
let entry = distinct_entry(n, now_ms / 1000);
e.ingest_added(&sender, vec![entry], now_ms);
}
assert!(
e.received_count(&sender) <= PEX_MAX_RECEIVED_PER_LINK,
"received map must stay bounded at PEX_MAX_RECEIVED_PER_LINK, got {}",
e.received_count(&sender)
);
assert!(
!e.links[&sender].received.contains_key(&hex_id(2)),
"the oldest entry should have been evicted"
);
let newest = hex_id(1 + total as u32);
assert!(
e.links[&sender].received.contains_key(&newest),
"the newest entry must survive eviction"
);
}
#[test]
fn hints_map_is_capped_globally_with_eviction() {
let local = hex_id(0);
let mut e = eng(&local);
let now_ms = 1_000_000_000_u64;
let total = PEX_MAX_HINTS + 500;
for n in 0..total as u32 {
let sender = hex_id(1_000_000 + n);
ensure_link(&mut e, &sender);
let entry = distinct_entry(2_000_000 + n, now_ms / 1000 + u64::from(n));
e.ingest_added(&sender, vec![entry], now_ms);
}
assert!(
e.hints_count() <= PEX_MAX_HINTS,
"hints map must stay bounded at PEX_MAX_HINTS, got {}",
e.hints_count()
);
assert!(
e.current_hint(&hex_id(2_000_000)).is_none(),
"the oldest hint should have been evicted"
);
let newest_peer = hex_id(2_000_000 + total as u32 - 1);
assert!(
e.current_hint(&newest_peer).is_some(),
"the newest hint must survive eviction"
);
}
#[test]
fn muting_a_direction_frees_its_received_and_sourced_hints() {
let local = hex_id(0);
let sender = hex_id(1);
let mut e = eng(&local);
let now_ms = 1_000_000_000_u64;
ensure_link(&mut e, &sender);
e.ingest_added(&sender, vec![distinct_entry(2, now_ms / 1000)], now_ms);
assert_eq!(e.received_count(&sender), 1);
assert!(e.current_hint(&hex_id(2)).is_some());
for _ in 0..3 {
e.strike(&sender, PexErrorCode::ProtocolViolation);
}
assert!(e.is_muted(&sender));
assert_eq!(
e.received_count(&sender),
0,
"received state must be freed when the direction is muted"
);
assert!(
e.current_hint(&hex_id(2)).is_none(),
"hints sourced from a now-muted link must be cleared"
);
}
}
#[cfg(test)]
mod advertisable_cache_tests {
use super::*;
use crate::entry::{Address, Provenance};
fn hex_id(n: u32) -> String {
format!("{n:064x}")
}
fn eng(local: &str) -> PexEngine {
PexEngine::new(PexConfig::new(local.to_string(), "mainnet".to_string()).with_jitter(false))
}
fn known_entry(n: u32, last_seen: u64) -> PeerEntry {
PeerEntry::new(hex_id(n), "mainnet", last_seen, Provenance::Direct)
.with_address(Address::direct("203.0.113.7", 9444))
}
#[test]
fn tick_rebuilds_advertisable_base_once_for_many_links() {
let mut e = eng(&hex_id(0));
for n in 100..110 {
e.upsert_known(known_entry(n, 1_000));
}
for n in 0..20u32 {
e.link_up(&hex_id(n), 1_000_000);
}
assert_eq!(
e.advertisable_rebuild_count(),
1,
"20 link_ups at the same now_secs must share a single advertisable rebuild, got {}",
e.advertisable_rebuild_count()
);
let _out = e.tick(1_000_000 + 61_000);
assert_eq!(
e.advertisable_rebuild_count(),
2,
"one tick covering 20 links must add exactly one more rebuild (now_secs changed once), got {}",
e.advertisable_rebuild_count()
);
}
#[test]
fn cache_invalidates_on_known_mutation_even_at_same_now_secs() {
let mut e = eng(&hex_id(0));
e.upsert_known(known_entry(1, 1_000));
let now_ms = 1_000_000;
let first = e.advertisable_for(&hex_id(99), now_ms / 1000);
assert_eq!(first.len(), 1);
assert_eq!(e.advertisable_rebuild_count(), 1);
e.upsert_known(known_entry(2, 1_000));
let second = e.advertisable_for(&hex_id(99), now_ms / 1000);
assert_eq!(second.len(), 2, "the newly upserted peer must appear");
assert_eq!(
e.advertisable_rebuild_count(),
2,
"a known mutation must invalidate the cache even at the same now_secs"
);
let third = e.advertisable_for(&hex_id(98), now_ms / 1000);
assert_eq!(third.len(), 2);
assert_eq!(
e.advertisable_rebuild_count(),
2,
"an unchanged known set at the same now_secs must reuse the cached build"
);
}
#[test]
fn cached_base_still_excludes_each_links_own_partner() {
let mut e = eng(&hex_id(0));
e.upsert_known(known_entry(1, 1_000));
e.upsert_known(known_entry(2, 1_000));
let now_secs = 1_000;
let for_1 = e.advertisable_for(&hex_id(1), now_secs);
assert!(
for_1.iter().all(|p| p.peer_id != hex_id(1)),
"peer 1's own link must never be advertised back to it"
);
assert!(for_1.iter().any(|p| p.peer_id == hex_id(2)));
let for_2 = e.advertisable_for(&hex_id(2), now_secs);
assert!(for_2.iter().all(|p| p.peer_id != hex_id(2)));
assert!(for_2.iter().any(|p| p.peer_id == hex_id(1)));
}
}