From 65c341bff18885d8a020433ba282018a316c1827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:40:12 -0300 Subject: [PATCH] feat(cli): add the --discovery.* option group The discv5 work needs three operator knobs, and landing them on their own keeps the implementation PR to the p2p crate. The flags parse and validate here; nothing reads them yet. `--discovery.enable` is off by default: nothing else on the lean network speaks discv5 yet. `--discovery.port` is the discv5 UDP socket, separate from the QUIC port `--gossipsub-port` binds, and `validate_discovery` rejects the two colliding rather than letting it surface at bind time as an EADDRINUSE on whichever socket loses the race. Both default to 9000, so enabling discovery means moving one of them. `--discovery.advertise-ip` separates what the node binds from what it publishes, for a host whose reachable address is not the wildcard it listens on. --- bin/ethlambda/src/cli.rs | 46 +++++++++++++++++++++++++++++++++++++++ bin/ethlambda/src/main.rs | 1 + 2 files changed, 47 insertions(+) diff --git a/bin/ethlambda/src/cli.rs b/bin/ethlambda/src/cli.rs index 81208b67..30382a0e 100644 --- a/bin/ethlambda/src/cli.rs +++ b/bin/ethlambda/src/cli.rs @@ -113,6 +113,8 @@ pub(crate) struct CliOptions { /// `on_block`. #[arg(long, default_value = "3")] pub(crate) max_attestations_per_block: usize, + #[command(flatten)] + pub(crate) discovery: DiscoveryConfig, /// Shadow-simulator sim-cost + fake-XMSS flags (only under the /// `shadow-integration` feature). #[cfg(feature = "shadow-integration")] @@ -120,6 +122,50 @@ pub(crate) struct CliOptions { pub(crate) shadow: ShadowOptions, } +/// discv5 peer discovery. Off by default: nothing else on the lean network +/// speaks discv5 yet, so enabling it only finds other ethlambda nodes. +#[derive(Debug, clap::Args)] +pub(crate) struct DiscoveryConfig { + /// Enable discv5 peer discovery. + /// + /// Requires `--discovery.port` to differ from `--gossipsub-port`: both are + /// UDP sockets and they cannot share one port. + #[arg(long = "discovery.enable", default_value = "false")] + pub(crate) enable: bool, + /// UDP port for the discv5 socket. + /// + /// Independent of `--gossipsub-port`, which carries libp2p QUIC. Both + /// default to 9000, so enabling discovery means changing one of them. + #[arg(long = "discovery.port", default_value = "9000")] + pub(crate) port: u16, + /// IP address to advertise in the ENR. + /// + /// Defaults to the bind address, which is the wildcard `0.0.0.0` and is not + /// dialable as published. Set this to the address peers should reach this + /// node on: `127.0.0.1` for a local devnet, or the host's public address. + /// discv5's PONG-based IP voting may still replace it at runtime. + #[arg(long = "discovery.advertise-ip")] + pub(crate) advertise_ip: Option, +} + +impl CliOptions { + /// Reject a discovery port that collides with the QUIC port. + /// + /// Both are UDP. Without this the collision surfaces at bind time as an + /// opaque `EADDRINUSE` on whichever socket loses the race. + pub(crate) fn validate_discovery(&self) -> eyre::Result<()> { + if self.discovery.enable && self.discovery.port == self.gossipsub_port { + eyre::bail!( + "--discovery.port ({}) must differ from --gossipsub-port ({}): \ + both bind UDP and cannot share a port", + self.discovery.port, + self.gossipsub_port + ); + } + Ok(()) + } +} + /// Shadow-simulator sim-cost + fake-XMSS flags. Compiled only under the /// `shadow-integration` feature. #[cfg(feature = "shadow-integration")] diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 5b40ad37..b1d84a40 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -81,6 +81,7 @@ async fn main() -> eyre::Result<()> { .wrap_err("failed to set global tracing subscriber")?; let options = CliOptions::parse(); + options.validate_discovery()?; #[cfg(feature = "shadow-integration")] init_shadow_cost(&options.shadow);