fix(discv5): decouple rlpx & discv5 ipmode (#8080)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Emilia Hane
2024-05-06 19:36:08 +02:00
committed by GitHub
parent c70b17a554
commit 16f85c4339
8 changed files with 278 additions and 87 deletions

View File

@ -3,20 +3,26 @@
use std::{
collections::HashSet,
fmt::Debug,
net::{IpAddr, Ipv4Addr, SocketAddr},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
};
use derive_more::Display;
use discv5::ListenConfig;
use multiaddr::{Multiaddr, Protocol};
use reth_primitives::{Bytes, EnrForkIdEntry, ForkId, NodeRecord};
use tracing::warn;
use crate::{enr::discv4_id_to_multiaddr_id, filter::MustNotIncludeKeys, NetworkStackId};
/// The default address for discv5 via UDP.
/// The default address for discv5 via UDP is IPv4.
///
/// Default is 0.0.0.0, all interfaces. See [`discv5::ListenConfig`] default.
pub const DEFAULT_DISCOVERY_V5_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::UNSPECIFIED);
pub const DEFAULT_DISCOVERY_V5_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
/// The default IPv6 address for discv5 via UDP.
///
/// Default is ::, all interfaces.
pub const DEFAULT_DISCOVERY_V5_ADDR_IPV6: Ipv6Addr = Ipv6Addr::UNSPECIFIED;
/// The default port for discv5 via UDP.
///
@ -40,7 +46,7 @@ pub const DEFAULT_COUNT_BOOTSTRAP_LOOKUPS: u64 = 100;
pub const DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL: u64 = 5;
/// Builds a [`Config`].
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ConfigBuilder {
/// Config used by [`discv5::Discv5`]. Contains the discovery listen socket.
discv5_config: Option<discv5::Config>,
@ -51,10 +57,11 @@ pub struct ConfigBuilder {
///
/// Defaults to L1 mainnet if not set.
fork: Option<(&'static [u8], ForkId)>,
/// RLPx TCP port to advertise. Note: so long as `reth_network` handles [`NodeRecord`]s as
/// opposed to [`Enr`](enr::Enr)s, TCP is limited to same IP address as UDP, since
/// [`NodeRecord`] doesn't supply an extra field for and alternative TCP address.
tcp_port: u16,
/// RLPx TCP socket to advertise.
///
/// NOTE: IP address of RLPx socket overwrites IP address of same IP version in
/// [`discv5::ListenConfig`].
tcp_socket: SocketAddr,
/// List of `(key, rlp-encoded-value)` tuples that should be advertised in local node record
/// (in addition to tcp port, udp port and fork).
other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
@ -77,7 +84,7 @@ impl ConfigBuilder {
discv5_config,
bootstrap_nodes,
fork,
tcp_port,
tcp_socket,
other_enr_kv_pairs,
lookup_interval,
bootstrap_lookup_interval,
@ -89,7 +96,7 @@ impl ConfigBuilder {
discv5_config: Some(discv5_config),
bootstrap_nodes,
fork: fork.map(|(key, fork_id)| (key, fork_id.fork_id)),
tcp_port,
tcp_socket,
other_enr_kv_pairs,
lookup_interval: Some(lookup_interval),
bootstrap_lookup_interval: Some(bootstrap_lookup_interval),
@ -152,9 +159,11 @@ impl ConfigBuilder {
self
}
/// Sets the tcp port to advertise in the local [`Enr`](discv5::enr::Enr).
pub fn tcp_port(mut self, port: u16) -> Self {
self.tcp_port = port;
/// Sets the tcp socket to advertise in the local [`Enr`](discv5::enr::Enr). The IP address of
/// this socket will overwrite the discovery address of the same IP version, if one is
/// configured.
pub fn tcp_socket(mut self, socket: SocketAddr) -> Self {
self.tcp_socket = socket;
self
}
@ -201,7 +210,7 @@ impl ConfigBuilder {
discv5_config,
bootstrap_nodes,
fork,
tcp_port,
tcp_socket,
other_enr_kv_pairs,
lookup_interval,
bootstrap_lookup_interval,
@ -209,9 +218,12 @@ impl ConfigBuilder {
discovered_peer_filter,
} = self;
let discv5_config = discv5_config
let mut discv5_config = discv5_config
.unwrap_or_else(|| discv5::ConfigBuilder::new(ListenConfig::default()).build());
discv5_config.listen_config =
amend_listen_config_wrt_rlpx(&discv5_config.listen_config, tcp_socket.ip());
let fork = fork.map(|(key, fork_id)| (key, fork_id.into()));
let lookup_interval = lookup_interval.unwrap_or(DEFAULT_SECONDS_LOOKUP_INTERVAL);
@ -227,7 +239,7 @@ impl ConfigBuilder {
discv5_config,
bootstrap_nodes,
fork,
tcp_port,
tcp_socket,
other_enr_kv_pairs,
lookup_interval,
bootstrap_lookup_interval,
@ -248,8 +260,11 @@ pub struct Config {
/// Fork kv-pair to set in local node record. Identifies which network/chain/fork the node
/// belongs, e.g. `(b"opstack", ChainId)` or `(b"eth", [ForkId])`.
pub(super) fork: Option<(&'static [u8], EnrForkIdEntry)>,
/// RLPx TCP port to advertise.
pub(super) tcp_port: u16,
/// RLPx TCP socket to advertise.
///
/// NOTE: IP address of RLPx socket overwrites IP address of same IP version in
/// [`discv5::ListenConfig`].
pub(super) tcp_socket: SocketAddr,
/// Additional kv-pairs (besides tcp port, udp port and fork) that should be advertised to
/// peers by including in local node record.
pub(super) other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
@ -266,9 +281,20 @@ pub struct Config {
}
impl Config {
/// Returns a new [`ConfigBuilder`], with the RLPx TCP port set to the given port.
pub fn builder(rlpx_tcp_port: u16) -> ConfigBuilder {
ConfigBuilder::default().tcp_port(rlpx_tcp_port)
/// Returns a new [`ConfigBuilder`], with the RLPx TCP port and IP version configured w.r.t.
/// the given socket.
pub fn builder(rlpx_tcp_socket: SocketAddr) -> ConfigBuilder {
ConfigBuilder {
discv5_config: None,
bootstrap_nodes: HashSet::new(),
fork: None,
tcp_socket: rlpx_tcp_socket,
other_enr_kv_pairs: Vec::new(),
lookup_interval: None,
bootstrap_lookup_interval: None,
bootstrap_lookup_countdown: None,
discovered_peer_filter: None,
}
}
}
@ -286,12 +312,104 @@ impl Config {
/// Returns the RLPx (TCP) socket contained in the [`discv5::Config`]. This socket will be
/// advertised to peers in the local [`Enr`](discv5::enr::Enr).
pub fn rlpx_socket(&self) -> SocketAddr {
let port = self.tcp_port;
match self.discv5_config.listen_config {
ListenConfig::Ipv4 { ip, .. } => (ip, port).into(),
ListenConfig::Ipv6 { ip, .. } => (ip, port).into(),
ListenConfig::DualStack { ipv4, .. } => (ipv4, port).into(),
pub fn rlpx_socket(&self) -> &SocketAddr {
&self.tcp_socket
}
}
/// Returns the IPv4 discovery socket if one is configured.
pub fn ipv4(listen_config: &ListenConfig) -> Option<SocketAddrV4> {
match listen_config {
ListenConfig::Ipv4 { ip, port } |
ListenConfig::DualStack { ipv4: ip, ipv4_port: port, .. } => {
Some(SocketAddrV4::new(*ip, *port))
}
ListenConfig::Ipv6 { .. } => None,
}
}
/// Returns the IPv6 discovery socket if one is configured.
pub fn ipv6(listen_config: &ListenConfig) -> Option<SocketAddrV6> {
match listen_config {
ListenConfig::Ipv4 { .. } => None,
ListenConfig::Ipv6 { ip, port } |
ListenConfig::DualStack { ipv6: ip, ipv6_port: port, .. } => {
Some(SocketAddrV6::new(*ip, *port, 0, 0))
}
}
}
/// Returns the amended [`discv5::ListenConfig`] based on the RLPx IP address. The ENR is limited
/// to one IP address per IP version (atm, may become spec'd how to advertise different addresses).
/// The RLPx address overwrites the discv5 address w.r.t. IP version.
pub fn amend_listen_config_wrt_rlpx(
listen_config: &ListenConfig,
rlpx_addr: IpAddr,
) -> ListenConfig {
let discv5_socket_ipv4 = ipv4(listen_config);
let discv5_socket_ipv6 = ipv6(listen_config);
let discv5_port_ipv4 =
discv5_socket_ipv4.map(|socket| socket.port()).unwrap_or(DEFAULT_DISCOVERY_V5_PORT);
let discv5_addr_ipv4 = discv5_socket_ipv4.map(|socket| *socket.ip());
let discv5_port_ipv6 =
discv5_socket_ipv6.map(|socket| socket.port()).unwrap_or(DEFAULT_DISCOVERY_V5_PORT);
let discv5_addr_ipv6 = discv5_socket_ipv6.map(|socket| *socket.ip());
let (discv5_socket_ipv4, discv5_socket_ipv6) = discv5_sockets_wrt_rlpx_addr(
rlpx_addr,
discv5_addr_ipv4,
discv5_port_ipv4,
discv5_addr_ipv6,
discv5_port_ipv6,
);
ListenConfig::from_two_sockets(discv5_socket_ipv4, discv5_socket_ipv6)
}
/// Returns the sockets that can be used for discv5 with respect to the RLPx address. ENR specs only
/// acknowledge one address per IP version.
pub fn discv5_sockets_wrt_rlpx_addr(
rlpx_addr: IpAddr,
discv5_addr_ipv4: Option<Ipv4Addr>,
discv5_port_ipv4: u16,
discv5_addr_ipv6: Option<Ipv6Addr>,
discv5_port_ipv6: u16,
) -> (Option<SocketAddrV4>, Option<SocketAddrV6>) {
match rlpx_addr {
IpAddr::V4(rlpx_addr) => {
let discv5_socket_ipv6 =
discv5_addr_ipv6.map(|ip| SocketAddrV6::new(ip, discv5_port_ipv6, 0, 0));
if let Some(discv5_addr) = discv5_addr_ipv4 {
warn!(target: "discv5",
%discv5_addr,
%rlpx_addr,
"Overwriting discv5 IPv4 address with RLPx IPv4 address, limited to one advertised IP address per IP version"
);
}
// overwrite discv5 ipv4 addr with RLPx address. this is since there is no
// spec'd way to advertise a different address for rlpx and discovery in the
// ENR.
(Some(SocketAddrV4::new(rlpx_addr, discv5_port_ipv4)), discv5_socket_ipv6)
}
IpAddr::V6(rlpx_addr) => {
let discv5_socket_ipv4 =
discv5_addr_ipv4.map(|ip| SocketAddrV4::new(ip, discv5_port_ipv4));
if let Some(discv5_addr) = discv5_addr_ipv6 {
warn!(target: "discv5",
%discv5_addr,
%rlpx_addr,
"Overwriting discv5 IPv6 address with RLPx IPv6 address, limited to one advertised IP address per IP version"
);
}
// overwrite discv5 ipv6 addr with RLPx address. this is since there is no
// spec'd way to advertise a different address for rlpx and discovery in the
// ENR.
(discv5_socket_ipv4, Some(SocketAddrV6::new(rlpx_addr, discv5_port_ipv6, 0, 0)))
}
}
}
@ -351,7 +469,7 @@ mod test {
fn parse_boot_nodes() {
const OP_SEPOLIA_CL_BOOTNODES: &str ="enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg,enr:-J64QFa3qMsONLGphfjEkeYyF6Jkil_jCuJmm7_a42ckZeUQGLVzrzstZNb1dgBp1GGx9bzImq5VxJLP-BaptZThGiWGAYrTytOvgmlkgnY0gmlwhGsV-zeHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDahfSECTIS_cXyZ8IyNf4leANlZnrsMEWTkEYxf4GMCmDdGNwgiQGg3VkcIIkBg";
let config = Config::builder(30303)
let config = Config::builder((Ipv4Addr::UNSPECIFIED, 30303).into())
.add_cl_serialized_signed_boot_nodes(OP_SEPOLIA_CL_BOOTNODES)
.build();
@ -371,7 +489,7 @@ mod test {
#[test]
fn parse_enodes() {
let config = Config::builder(30303)
let config = Config::builder((Ipv4Addr::UNSPECIFIED, 30303).into())
.add_serialized_unsigned_boot_nodes(BOOT_NODES_OP_MAINNET_AND_BASE_MAINNET)
.build();
@ -382,4 +500,34 @@ mod test {
assert!(bootstrap_nodes.contains(&node.to_string()));
}
}
#[test]
fn overwrite_ipv4_addr() {
let rlpx_addr: Ipv4Addr = "192.168.0.1".parse().unwrap();
let listen_config = ListenConfig::default();
let amended_config = amend_listen_config_wrt_rlpx(&listen_config, rlpx_addr.into());
let config_socket_ipv4 = ipv4(&amended_config).unwrap();
assert_eq!(*config_socket_ipv4.ip(), rlpx_addr);
assert_eq!(config_socket_ipv4.port(), DEFAULT_DISCOVERY_V5_PORT);
assert_eq!(ipv6(&amended_config), ipv6(&listen_config));
}
#[test]
fn overwrite_ipv6_addr() {
let rlpx_addr: Ipv6Addr = "fe80::1".parse().unwrap();
let listen_config = ListenConfig::default();
let amended_config = amend_listen_config_wrt_rlpx(&listen_config, rlpx_addr.into());
let config_socket_ipv6 = ipv6(&amended_config).unwrap();
assert_eq!(*config_socket_ipv6.ip(), rlpx_addr);
assert_eq!(config_socket_ipv6.port(), DEFAULT_DISCOVERY_V5_PORT);
assert_eq!(ipv4(&amended_config), ipv4(&listen_config));
}
}

View File

@ -35,4 +35,7 @@ pub enum Error {
/// An error from underlying [`discv5::Discv5`] node.
#[error("sigp/discv5 error, {0}")]
Discv5Error(discv5::Error),
/// The [`ListenConfig`](discv5::ListenConfig) has been misconfigured.
#[error("misconfigured listen config, RLPx TCP address must also be supported by discv5")]
ListenConfigMisconfigured,
}

View File

@ -39,8 +39,8 @@ pub use discv5::{self, IpMode};
pub use config::{
BootNode, Config, ConfigBuilder, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_ADDR,
DEFAULT_DISCOVERY_V5_PORT, DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
DEFAULT_SECONDS_LOOKUP_INTERVAL,
DEFAULT_DISCOVERY_V5_ADDR_IPV6, DEFAULT_DISCOVERY_V5_PORT,
DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
};
pub use enr::enr_to_discv4_id;
pub use error::Error;
@ -66,8 +66,8 @@ pub const DEFAULT_MIN_TARGET_KBUCKET_INDEX: usize = 0;
pub struct Discv5 {
/// sigp/discv5 node.
discv5: Arc<discv5::Discv5>,
/// [`IpMode`] of the the node.
ip_mode: IpMode,
/// [`IpMode`] of the the RLPx network.
rlpx_ip_mode: IpMode,
/// Key used in kv-pair to ID chain, e.g. 'opstack' or 'eth'.
fork_key: Option<&'static [u8]>,
/// Filter applied to a discovered peers before passing it up to app.
@ -162,7 +162,7 @@ impl Discv5 {
//
// 1. make local enr from listen config
//
let (enr, bc_enr, fork_key, ip_mode) = build_local_enr(sk, &discv5_config);
let (enr, bc_enr, fork_key, rlpx_ip_mode) = build_local_enr(sk, &discv5_config);
trace!(target: "net::discv5",
?enr,
@ -214,7 +214,7 @@ impl Discv5 {
);
Ok((
Self { discv5, ip_mode, fork_key, discovered_peer_filter, metrics },
Self { discv5, rlpx_ip_mode, fork_key, discovered_peer_filter, metrics },
discv5_updates,
bc_enr,
))
@ -328,7 +328,7 @@ impl Discv5 {
}
/// Tries to convert an [`Enr`](discv5::Enr) into the backwards compatible type [`NodeRecord`],
/// w.r.t. local [`IpMode`]. Uses source socket as udp socket.
/// w.r.t. local RLPx [`IpMode`]. Uses source socket as udp socket.
pub fn try_into_reachable(
&self,
enr: &discv5::Enr,
@ -336,13 +336,15 @@ impl Discv5 {
) -> Result<NodeRecord, Error> {
let id = enr_to_discv4_id(enr).ok_or(Error::IncompatibleKeyType)?;
// since we, on bootstrap, set tcp4 in local ENR for `IpMode::Dual`, we prefer tcp4 here
// too
let Some(tcp_port) = (match self.ip_mode() {
IpMode::Ip4 | IpMode::DualStack => enr.tcp4(),
if enr.tcp4().is_none() && enr.tcp6().is_none() {
return Err(Error::UnreachableRlpx)
}
let Some(tcp_port) = (match self.rlpx_ip_mode {
IpMode::Ip4 => enr.tcp4(),
IpMode::Ip6 => enr.tcp6(),
_ => unimplemented!("dual-stack support not implemented for rlpx"),
}) else {
return Err(Error::IpVersionMismatchRlpx(self.ip_mode()))
return Err(Error::IpVersionMismatchRlpx(self.rlpx_ip_mode))
};
Ok(NodeRecord { address: socket.ip(), tcp_port, udp_port: socket.port(), id })
@ -385,9 +387,9 @@ impl Discv5 {
// Complementary
////////////////////////////////////////////////////////////////////////////////////////////////
/// Returns the [`IpMode`] of the local node.
/// Returns the RLPx [`IpMode`] of the local node.
pub fn ip_mode(&self) -> IpMode {
self.ip_mode
self.rlpx_ip_mode
}
/// Returns the key to use to identify the [`ForkId`] kv-pair on the [`Enr`](discv5::Enr).
@ -418,43 +420,45 @@ pub fn build_local_enr(
) -> (Enr<SecretKey>, NodeRecord, Option<&'static [u8]>, IpMode) {
let mut builder = discv5::enr::Enr::builder();
let Config { discv5_config, fork, tcp_port, other_enr_kv_pairs, .. } = config;
let Config { discv5_config, fork, tcp_socket, other_enr_kv_pairs, .. } = config;
let (ip_mode, socket) = match discv5_config.listen_config {
let socket = match discv5_config.listen_config {
ListenConfig::Ipv4 { ip, port } => {
if ip != Ipv4Addr::UNSPECIFIED {
builder.ip4(ip);
}
builder.udp4(port);
builder.tcp4(*tcp_port);
builder.tcp4(tcp_socket.port());
(IpMode::Ip4, (ip, port).into())
(ip, port).into()
}
ListenConfig::Ipv6 { ip, port } => {
if ip != Ipv6Addr::UNSPECIFIED {
builder.ip6(ip);
}
builder.udp6(port);
builder.tcp6(*tcp_port);
builder.tcp6(tcp_socket.port());
(IpMode::Ip6, (ip, port).into())
(ip, port).into()
}
ListenConfig::DualStack { ipv4, ipv4_port, ipv6, ipv6_port } => {
if ipv4 != Ipv4Addr::UNSPECIFIED {
builder.ip4(ipv4);
}
builder.udp4(ipv4_port);
builder.tcp4(*tcp_port);
builder.tcp4(tcp_socket.port());
if ipv6 != Ipv6Addr::UNSPECIFIED {
builder.ip6(ipv6);
}
builder.udp6(ipv6_port);
(IpMode::DualStack, (ipv6, ipv6_port).into())
(ipv6, ipv6_port).into()
}
};
let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
// identifies which network node is on
let network_stack_id = fork.as_ref().map(|(network_stack_id, fork_value)| {
builder.add_value_rlp(network_stack_id, alloy_rlp::encode(fork_value).into());
@ -473,7 +477,7 @@ pub fn build_local_enr(
// backwards compatible enr
let bc_enr = NodeRecord::from_secret_key(socket, sk);
(enr, bc_enr, network_stack_id, ip_mode)
(enr, bc_enr, network_stack_id, rlpx_ip_mode)
}
/// Bootstraps underlying [`discv5::Discv5`] node with configured peers.
@ -660,7 +664,7 @@ mod test {
)
.unwrap(),
),
ip_mode: IpMode::Ip4,
rlpx_ip_mode: IpMode::Ip4,
fork_key: None,
discovered_peer_filter: MustNotIncludeKeys::default(),
metrics: Discv5Metrics::default(),
@ -673,9 +677,10 @@ mod test {
let secret_key = SecretKey::new(&mut thread_rng());
let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
let discv5_listen_config = ListenConfig::from(discv5_addr);
let discv5_config = Config::builder(30303)
let discv5_config = Config::builder(rlpx_addr)
.discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
.build();
@ -867,7 +872,9 @@ mod test {
const TCP_PORT: u16 = 30303;
let fork_id = MAINNET.latest_fork_id();
let config = Config::builder(TCP_PORT).fork(NetworkStackId::ETH, fork_id).build();
let config = Config::builder((Ipv4Addr::UNSPECIFIED, TCP_PORT).into())
.fork(NetworkStackId::ETH, fork_id)
.build();
let sk = SecretKey::new(&mut thread_rng());
let (enr, _, _, _) = build_local_enr(&sk, &config);