Skip to content

Commit 6ec0b7a

Browse files
committed
refactor(p2p): read a NodeRecord's fields through one set of helpers
`admit` and `parse_enr` each re-derived the IPv4-over-IPv6 preference and the secp256k1-to-libp2p key decode. Both answer "who does this record belong to, and where do we reach them", so letting the two drift would mean the bootnode parser and the admission filter disagreeing about the same ENR. They now share `read_ip`/`read_public_key`, next to the `read_quic_port` they already shared. Three smaller things in the same pass, none of them behaviour changes: `forget_discovered_peer` was the only inherent `P2PServer` method defined outside `lib.rs`; every other submodule reaches the actor through a free function taking `&mut P2PServer`, so `grep 'impl P2PServer'` no longer missed part of its mutating surface. The dial loop cloned the peer-table ref and the filter on every tick but only used them when refilling an empty candidate queue, so the clones now happen under that condition. Discovery items nothing outside the crate names drop to `pub(crate)`. Only `DiscoverySpawnConfig`, `DiscoveryError` and `DEFAULT_DISCOVERY_TARGET_PEERS` cross into `bin/ethlambda`; the rest read as API with no consumer.
1 parent 419b526 commit 6ec0b7a

5 files changed

Lines changed: 91 additions & 79 deletions

File tree

crates/net/p2p/src/discovery/admission.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,26 @@
1818
//! ([`rank_by_uncovered_subnets`]).
1919
2020
use std::collections::HashSet;
21-
use std::net::IpAddr;
2221

2322
use ethrex_p2p::peer_filter::PeerFilter;
2423
use ethrex_p2p::types::NodeRecord;
2524
use libp2p::{Multiaddr, PeerId};
2625
use libssz::SszDecode;
2726
use tracing::debug;
2827

29-
use super::enr::{ATTNETS_ENR_KEY, ETH2_ENR_KEY, EnrForkId, read_quic_port, subnets_from_attnets};
28+
use super::enr::{
29+
ATTNETS_ENR_KEY, ETH2_ENR_KEY, EnrForkId, read_ip, read_public_key, read_quic_port,
30+
subnets_from_attnets,
31+
};
3032
use crate::quic_multiaddr;
3133

3234
/// A peer that passed admission and is ready to dial.
3335
#[derive(Debug, Clone, PartialEq)]
34-
pub struct DiscoveredPeer {
35-
pub peer_id: PeerId,
36-
pub addr: Multiaddr,
36+
pub(crate) struct DiscoveredPeer {
37+
pub(crate) peer_id: PeerId,
38+
pub(crate) addr: Multiaddr,
3739
/// Attestation subnets the peer advertises in `attnets`.
38-
pub subnets: Vec<u64>,
40+
pub(crate) subnets: Vec<u64>,
3941
}
4042

4143
/// Why a discovered peer was turned away.
@@ -44,7 +46,7 @@ pub struct DiscoveredPeer {
4446
/// record, so a peer that adds a `quic` entry or gains an address through
4547
/// discv5's IP voting is reconsidered without restarting the process.
4648
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47-
pub enum RejectReason {
49+
pub(crate) enum RejectReason {
4850
/// No `eth2` entry, or one that does not decode. Cannot be our network.
4951
MissingForkId,
5052
/// On a different network.
@@ -73,7 +75,7 @@ pub struct LeanFilter {
7375
}
7476

7577
impl LeanFilter {
76-
pub fn new(fork_id: EnrForkId, attestation_committee_count: u64) -> Self {
78+
pub(crate) fn new(fork_id: EnrForkId, attestation_committee_count: u64) -> Self {
7779
Self {
7880
fork_id,
7981
attestation_committee_count,
@@ -88,7 +90,7 @@ impl LeanFilter {
8890
/// `expect` because the two are only guaranteed to agree while the record is
8991
/// unchanged, and the peer table hands out clones: a caller that reaches
9092
/// this with an arbitrary record should get nothing to dial, not a panic.
91-
pub fn dial_target(&self, record: &NodeRecord) -> Option<DiscoveredPeer> {
93+
pub(crate) fn dial_target(&self, record: &NodeRecord) -> Option<DiscoveredPeer> {
9294
admit(record, &self.fork_id, self.attestation_committee_count).ok()
9395
}
9496
}
@@ -144,17 +146,10 @@ fn admit(
144146

145147
let quic_port = read_quic_port(record).ok_or(RejectReason::NoQuicPort)?;
146148

147-
let public_key_bytes = pairs.secp256k1.ok_or(RejectReason::BadPublicKey)?;
148-
let public_key =
149-
libp2p::identity::secp256k1::PublicKey::try_from_bytes(public_key_bytes.as_bytes())
150-
.map_err(|_| RejectReason::BadPublicKey)?;
149+
let public_key = read_public_key(pairs).ok_or(RejectReason::BadPublicKey)?;
151150
let peer_id = PeerId::from_public_key(&libp2p::identity::PublicKey::from(public_key));
152151

153-
let ip = pairs
154-
.ip
155-
.map(IpAddr::from)
156-
.or_else(|| pairs.ip6.map(IpAddr::from))
157-
.ok_or(RejectReason::MissingAddress)?;
152+
let ip = read_ip(pairs).ok_or(RejectReason::MissingAddress)?;
158153

159154
let subnets = pairs
160155
.extra(ATTNETS_ENR_KEY)
@@ -173,7 +168,7 @@ fn admit(
173168
///
174169
/// A candidate advertising no subnets scores zero and sorts last, but is never
175170
/// dropped: with few peers, any peer is better than none.
176-
pub fn rank_by_uncovered_subnets(candidates: &mut [DiscoveredPeer], covered: &HashSet<u64>) {
171+
pub(crate) fn rank_by_uncovered_subnets(candidates: &mut [DiscoveredPeer], covered: &HashSet<u64>) {
177172
candidates.sort_by_key(|candidate| {
178173
std::cmp::Reverse(
179174
candidate
@@ -192,7 +187,7 @@ mod tests {
192187
use ethrex_p2p::utils::public_key_from_signing_key;
193188
use libssz::SszEncode;
194189
use std::collections::HashSet;
195-
use std::net::Ipv4Addr;
190+
use std::net::{IpAddr, Ipv4Addr};
196191

197192
use super::super::enr::{FAR_FUTURE_EPOCH, QUIC_ENR_KEY, encode_attnets};
198193

crates/net/p2p/src/discovery/dial.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,37 +44,37 @@ impl DiscoveryState {
4444
}
4545
}
4646

47-
impl P2PServer {
48-
/// Drop a peer's discovery bookkeeping.
49-
///
50-
/// Called from both teardown paths — a connection that closed and a dial
51-
/// that never established — so the map cannot outlive the peers in it and
52-
/// [`covered_subnets`] cannot credit a subnet to someone who left. A no-op
53-
/// when discovery is off.
54-
pub(crate) fn forget_discovered_peer(&mut self, peer_id: &PeerId) {
55-
if let Some(discovery) = self.discovery.as_mut() {
56-
discovery.peer_attnets.remove(peer_id);
57-
}
47+
/// Drop a peer's discovery bookkeeping.
48+
///
49+
/// Called from both teardown paths — a connection that closed and a dial that
50+
/// never established — so the map cannot outlive the peers in it and
51+
/// [`covered_subnets`] cannot credit a subnet to someone who left. A no-op when
52+
/// discovery is off.
53+
pub(crate) fn forget_discovered_peer(server: &mut P2PServer, peer_id: &PeerId) {
54+
if let Some(discovery) = server.discovery.as_mut() {
55+
discovery.peer_attnets.remove(peer_id);
5856
}
5957
}
6058

6159
/// One tick of the dial loop. A no-op when discovery is disabled.
6260
pub(crate) async fn dial_tick(server: &mut P2PServer) {
6361
// Snapshot what the refill needs before any `.await`, so no borrow of
6462
// `server.discovery` has to live across the async boundary. Both are handle
65-
// clones: an actor ref and two `Copy` fields.
63+
// clones: an actor ref and two `Copy` fields, taken only when a refill is
64+
// actually due rather than on every tick that just drains the queue.
6665
let Some(discovery) = server.discovery.as_ref() else {
6766
return;
6867
};
6968
if server.connected_peers.len() >= discovery.target_peers {
7069
return;
7170
}
72-
let peer_table = discovery.peer_table.clone();
73-
let filter = discovery.filter.clone();
74-
let mut admitted = if discovery.candidates.is_empty() {
75-
draw_candidates(&peer_table, &filter).await
76-
} else {
77-
Vec::new()
71+
let refill = discovery
72+
.candidates
73+
.is_empty()
74+
.then(|| (discovery.peer_table.clone(), discovery.filter.clone()));
75+
let mut admitted = match refill {
76+
Some((peer_table, filter)) => draw_candidates(&peer_table, &filter).await,
77+
None => Vec::new(),
7878
};
7979

8080
let Some(discovery) = server.discovery.as_mut() else {

crates/net/p2p/src/discovery/enr.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ pub(crate) const FAR_FUTURE_EPOCH: u64 = u64::MAX;
4343
/// The `eth2` ENR entry: SSZ, 16 bytes, byte-identical to the beacon-chain
4444
/// `ENRForkID` container.
4545
#[derive(Debug, Clone, Copy, PartialEq, Eq, SszEncode, SszDecode)]
46-
pub struct EnrForkId {
47-
pub fork_digest: [u8; 4],
48-
pub next_fork_version: [u8; 4],
49-
pub next_fork_epoch: u64,
46+
pub(crate) struct EnrForkId {
47+
pub(crate) fork_digest: [u8; 4],
48+
pub(crate) next_fork_version: [u8; 4],
49+
pub(crate) next_fork_epoch: u64,
5050
}
5151

5252
impl EnrForkId {
5353
/// This node's fork id. Constant for the lifetime of the process.
54-
pub fn local() -> Self {
54+
pub(crate) fn local() -> Self {
5555
Self {
5656
fork_digest: fork_digest(),
5757
next_fork_version: NEXT_FORK_VERSION,
@@ -171,6 +171,31 @@ pub(crate) fn build_local_enr(params: &LocalEnrParams) -> Result<NodeRecord, Dis
171171
.map_err(DiscoveryError::BuildEnr)
172172
}
173173

174+
/// The address a record advertises, preferring IPv4 when it carries both.
175+
///
176+
/// `None` for a record with neither `ip` nor `ip6`, which names no host to
177+
/// reach. Shared with the bootnode parser so both readers agree on which family
178+
/// wins.
179+
pub(crate) fn read_ip(pairs: &NodeRecordPairs) -> Option<IpAddr> {
180+
pairs
181+
.ip
182+
.map(IpAddr::from)
183+
.or_else(|| pairs.ip6.map(IpAddr::from))
184+
}
185+
186+
/// The `secp256k1` entry as a libp2p key, or `None` when absent or not a valid
187+
/// compressed point.
188+
///
189+
/// libp2p derives the peer id from this key, so the bootnode parser and the
190+
/// admission filter must decode it the same way or they would disagree about who
191+
/// a record belongs to.
192+
pub(crate) fn read_public_key(
193+
pairs: &NodeRecordPairs,
194+
) -> Option<libp2p::identity::secp256k1::PublicKey> {
195+
let bytes = pairs.secp256k1?;
196+
libp2p::identity::secp256k1::PublicKey::try_from_bytes(bytes.as_bytes()).ok()
197+
}
198+
174199
/// The advertised libp2p QUIC port, if it is one we could dial.
175200
///
176201
/// `None` covers an absent entry, an encoding `extra_int` cannot read (including

crates/net/p2p/src/discovery/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ pub struct DiscoverySpawnConfig {
8080
/// What the P2P actor needs from a running discovery server.
8181
pub struct DiscoveryHandle {
8282
pub peer_table: PeerTable,
83-
/// This node's ENR as an `enr:`-prefixed string, for logs and the RPC
84-
/// identity endpoint. Reflects startup state; discv5 may bump the sequence
85-
/// number later if PONG voting changes our external IP.
83+
/// This node's ENR as an `enr:`-prefixed string. `spawn_discovery` already
84+
/// logs it; this copy is what the tests assert the published record against,
85+
/// and what a future RPC identity endpoint would read. Reflects startup
86+
/// state; discv5 may bump the sequence number later if PONG voting changes
87+
/// our external IP.
8688
pub local_enr: String,
8789
/// The admission policy the peer table judges records with, kept so the dial
8890
/// loop can apply the same rules when it turns a contact into a dial target.

crates/net/p2p/src/lib.rs

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ use tracing::{debug, info, trace, warn};
3737
use crate::{
3838
discovery::{
3939
DISCOVERY_DIAL_INTERVAL, DiscoveryError, DiscoverySpawnConfig,
40-
dial::{DiscoveryState, dial_tick},
41-
enr::read_quic_port,
40+
dial::{DiscoveryState, dial_tick, forget_discovered_peer},
41+
enr::{read_ip, read_public_key, read_quic_port},
4242
spawn_discovery,
4343
},
4444
gossipsub::{
@@ -681,7 +681,7 @@ async fn handle_swarm_event(
681681
};
682682
if num_established == 0 {
683683
server.connected_peers.remove(&peer_id);
684-
server.forget_discovered_peer(&peer_id);
684+
forget_discovered_peer(server, &peer_id);
685685
let peer_count = server.connected_peers.len();
686686
metrics::notify_peer_disconnected(
687687
server.resolve_node_name(Some(&peer_id)),
@@ -723,24 +723,23 @@ async fn handle_swarm_event(
723723
);
724724
warn!(?peer_id, %error, "Outgoing connection error");
725725

726-
// A dial that never establishes ends up here rather than in
727-
// `ConnectionClosed`, so this is the only place a peer we dialed but
728-
// never connected to can be forgotten.
729726
if let Some(pid) = peer_id {
730-
server.forget_discovered_peer(&pid);
731-
}
732-
733-
// Schedule redial if this was a bootnode
734-
if let Some(pid) = peer_id
735-
&& server.bootnode_addrs.contains_key(&pid)
736-
&& !server.connected_peers.contains(&pid)
737-
{
738-
send_after(
739-
Duration::from_secs(PEER_REDIAL_INTERVAL_SECS),
740-
ctx.clone(),
741-
p2p_protocol::RetryPeerRedial { peer_id: pid },
742-
);
743-
info!(%pid, "Scheduled bootnode redial after connection error");
727+
// A dial that never establishes ends up here rather than in
728+
// `ConnectionClosed`, so this is the only place a peer we dialed
729+
// but never connected to can be forgotten.
730+
forget_discovered_peer(server, &pid);
731+
732+
// Schedule redial if this was a bootnode
733+
if server.bootnode_addrs.contains_key(&pid)
734+
&& !server.connected_peers.contains(&pid)
735+
{
736+
send_after(
737+
Duration::from_secs(PEER_REDIAL_INTERVAL_SECS),
738+
ctx.clone(),
739+
p2p_protocol::RetryPeerRedial { peer_id: pid },
740+
);
741+
info!(%pid, "Scheduled bootnode redial after connection error");
742+
}
744743
}
745744
}
746745
SwarmEvent::IncomingConnectionError { peer_id, error, .. } => {
@@ -862,19 +861,10 @@ fn parse_enr(enr_str: &str) -> Result<Bootnode, String> {
862861
// `build_swarm` skip it when it picks static dial targets.
863862
let quic_port = read_quic_port(&record);
864863

865-
let public_key_bytes = pairs
866-
.secp256k1
867-
.ok_or_else(|| "node record missing public key".to_string())?;
868-
let public_key =
869-
libp2p::identity::secp256k1::PublicKey::try_from_bytes(public_key_bytes.as_bytes())
870-
.map_err(|err| format!("bad secp256k1 key: {err}"))?;
871-
872-
// Prefer IPv4 if both are present.
873-
let ip = pairs
874-
.ip
875-
.map(IpAddr::from)
876-
.or_else(|| pairs.ip6.map(IpAddr::from))
877-
.ok_or_else(|| "node record missing IP address".to_string())?;
864+
let public_key = read_public_key(pairs)
865+
.ok_or_else(|| "node record missing or malformed public key".to_string())?;
866+
867+
let ip = read_ip(pairs).ok_or_else(|| "node record missing IP address".to_string())?;
878868

879869
// `quic` and `udp` are independently optional, but a record with neither is
880870
// reachable by nothing we speak: it can be neither dialed nor seeded. Drop

0 commit comments

Comments
 (0)