Repository: paradigmxyz/reth
Pull request #15347: feat(cli): add bootnode command
Author: login: frankudoags
Labels: name: D-good-first-issue
description: Nice and easy! A great choice to get started
name: A-networking
description: Related to networking in general
Created: 2025-03-28T07:55:50Z | Merged: 2025-04-18T11:57:40Z

closes #15324

Linked issue #15324: Add bootnode command
### Describe the feature

having a bootnode command is still useful especially for opstack for private networks
https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48

We should introduce a new command that runs the discovery stack in a similar way, with the same flags (except the log level ones)

for this we need

a new command in

https://github.com/paradigmxyz/reth/blob/9bcd37f2a0ac6f51c5559654e947713bcba5f395/crates/cli/commands/src/db/mod.rs#L30-L30

and an implementation in a new file.

The setup should be similar to how we launch the network:

https://github.com/paradigmxyz/reth/blob/9bcd37f2a0ac6f51c5559654e947713bcba5f395/crates/net/network/src/discovery.rs#L86-L101

### Additional context

_No response_
Linked issue comments: user: login: frankudoags
created_at: 2025-03-27T11:41:42Z
body: On it 😁
author_association: CONTRIBUTOR

Diff:
diff --git a/Cargo.lock b/Cargo.lock
index c52b128af36..353daa74ce4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7127,6 +7127,7 @@ dependencies = [
  "reth-db-api",
  "reth-db-common",
  "reth-discv4",
+ "reth-discv5",
  "reth-downloaders",
  "reth-ecies",
  "reth-eth-wire",
@@ -7135,6 +7136,7 @@ dependencies = [
  "reth-evm",
  "reth-exex",
  "reth-fs-util",
+ "reth-net-nat",
  "reth-network",
  "reth-network-p2p",
  "reth-network-peers",
@@ -7158,6 +7160,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tokio",
+ "tokio-stream",
  "toml",
  "tracing",
 ]
diff --git a/crates/cli/commands/Cargo.toml b/crates/cli/commands/Cargo.toml
index 67a28c741ac..e1cb010d43f 100644
--- a/crates/cli/commands/Cargo.toml
+++ b/crates/cli/commands/Cargo.toml
@@ -27,6 +27,7 @@ reth-eth-wire.workspace = true
 reth-evm.workspace = true
 reth-exex.workspace = true
 reth-fs-util.workspace = true
+reth-net-nat.workspace = true
 reth-network = { workspace = true, features = ["serde"] }
 reth-network-p2p.workspace = true
 reth-network-peers = { workspace = true, features = ["secp256k1"] }
@@ -47,6 +48,8 @@ reth-trie = { workspace = true, features = ["metrics"] }
 reth-trie-db = { workspace = true, features = ["metrics"] }
 reth-trie-common = { workspace = true, optional = true }
 reth-primitives-traits.workspace = true
+reth-discv4.workspace = true
+reth-discv5.workspace = true
 
 # ethereum
 alloy-eips.workspace = true
@@ -68,6 +71,7 @@ serde_json.workspace = true
 tracing.workspace = true
 backon.workspace = true
 secp256k1 = { workspace = true, features = ["global-context", "std", "recovery"] }
+tokio-stream.workspace = true
 
 # io
 fdlimit.workspace = true
@@ -84,7 +88,6 @@ arbitrary = { workspace = true, optional = true }
 proptest-arbitrary-interop = { workspace = true, optional = true }
 
 [dev-dependencies]
-reth-discv4.workspace = true
 reth-ethereum-cli.workspace = true
 
 [features]
diff --git a/crates/cli/commands/src/p2p/bootnode.rs b/crates/cli/commands/src/p2p/bootnode.rs
new file mode 100644
index 00000000000..9be60aca658
--- /dev/null
+++ b/crates/cli/commands/src/p2p/bootnode.rs
@@ -0,0 +1,107 @@
+//! Standalone bootnode command
+
+use clap::Parser;
+use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
+use reth_discv5::{discv5::Event, Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network_peers::NodeRecord;
+use std::{net::SocketAddr, str::FromStr};
+use tokio::select;
+use tokio_stream::StreamExt;
+use tracing::info;
+
+/// Satrt a discovery only bootnode.
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    /// Execute the bootnode command.
+    pub async fn execute(self) -> eyre::Result<()> {
+        info!("Bootnode started with config: {:?}", self);
+        let sk = reth_network::config::rng_secret_key();
+        let socket_addr = SocketAddr::from_str(&self.addr)?;
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+        let (_discv4, mut discv4_service) =
+            Discv4::bind(socket_addr, local_enr, sk, config).await?;
+
+        info!("Started discv4 at address:{:?}", socket_addr);
+
+        let mut discv4_updates = discv4_service.update_stream();
+        discv4_service.spawn();
+
+        // Optional discv5 update event listener if v5 is enabled
+        let mut discv5_updates = None;
+
+        if self.v5 {
+            info!("Starting discv5");
+            let config = Config::builder(socket_addr).build();
+            let (_discv5, updates, _local_enr_discv5) = Discv5::start(&sk, config).await?;
+            discv5_updates = Some(updates);
+        };
+
+        // event info loop for logging
+        loop {
+            select! {
+                //discv4 updates
+                update = discv4_updates.next() => {
+                    if let Some(update) = update {
+                        match update {
+                            DiscoveryUpdate::Added(record) => {
+                                info!("(Discv4) new peer added, peer_id={:?}", record.id);
+                            }
+                            DiscoveryUpdate::Removed(peer_id) => {
+                                info!("(Discv4) peer with peer-id={:?} removed", peer_id);
+                            }
+                            _ => {}
+                        }
+                    } else {
+                        info!("(Discv4) update stream ended.");
+                        break;
+                    }
+                }
+                //if discv5, discv5 update stream, else do nothing
+                update = async {
+                    if let Some(updates) = &mut discv5_updates {
+                        updates.recv().await
+                    } else {
+                        futures::future::pending().await
+                    }
+                } => {
+                    if let Some(update) = update {
+                     if let Event::SessionEstablished(enr, _) = update {
+                            info!("(Discv5) new peer added, peer_id={:?}", enr.id());
+                        }
+                    } else {
+                        info!("(Discv5) update stream ended.");
+                        break;
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/cli/commands/src/p2p/mod.rs b/crates/cli/commands/src/p2p/mod.rs
index 41a9b7b65a9..bb4c708d926 100644
--- a/crates/cli/commands/src/p2p/mod.rs
+++ b/crates/cli/commands/src/p2p/mod.rs
@@ -16,6 +16,7 @@ use reth_node_core::{
     utils::get_single_header,
 };
 
+pub mod bootnode;
 mod rlpx;
 
 /// `reth p2p` command


Reviews:
user: login: mattsse
body: good start, the next step would micking the discv4 setup, like:

https://github.com/paradigmxyz/reth/blob/6260fe94b764cce597ffe10b3fb9c55fed2acb7d/crates/net/network/src/discovery.rs#L86-L99

for this we need to configure a discv4config and populate things like the port

and generate a new secretkey

then bind it and simply await

if v5 is provided as bool arg, we also need a setup for:

https://github.com/paradigmxyz/reth/blob/6260fe94b764cce597ffe10b3fb9c55fed2acb7d/crates/net/network/src/discovery.rs#L101-L105

lmk if these instructions are sufficient, do also checkout the discv4 code:

https://github.com/paradigmxyz/reth/blob/6260fe94b764cce597ffe10b3fb9c55fed2acb7d/crates/net/discv4/src/lib.rs#L223-L233
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:37:37Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:04:21Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:04:29Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:04:41Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:04:49Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:05:00Z
user: login: mattsse
body: nice, 

this is compiling just fine for me.

have a few suggestions, but feature wise this is looking great
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T22:51:30Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T16:52:13Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T16:52:29Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T16:52:39Z
user: login: mattsse
body: cool, only have one more request for observability

the rand issue we'll figure out
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T16:33:56Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T18:42:35Z
user: login: mattsse
body: nice, last structural nits
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:34:36Z
user: login: frankudoags
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T07:58:11Z
user: login: mattsse
body: ty!
fixes the last warnings by exporting the type.

next step would be adding this to the cli structs
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-18T11:32:06Z

Review comments:
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: I think we can remove this one
created_at: 2025-03-28T16:32:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: I think we can also ignore this one
created_at: 2025-03-28T16:33:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: this one should be the same as:

https://github.com/paradigmxyz/reth/blob/6260fe94b764cce597ffe10b3fb9c55fed2acb7d/crates/node/core/src/args/network.rs#L90-L92
created_at: 2025-03-28T16:33:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
+
+    /// Restrict network communication to given IP networks (CIDR masks).
+    #[arg(long, default_value = "")]
+    pub net_restrict: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: this we can also remove
created_at: 2025-03-28T16:33:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
+
+    /// Restrict network communication to given IP networks (CIDR masks).
+    #[arg(long, default_value = "")]
+    pub net_restrict: String,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub fn execute(self) {
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: we need this to be async
created_at: 2025-03-28T16:34:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
+
+    /// Restrict network communication to given IP networks (CIDR masks).
+    #[arg(long, default_value = "")]
+    pub net_restrict: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-03-28T17:04:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
+
+    /// Restrict network communication to given IP networks (CIDR masks).
+    #[arg(long, default_value = "")]
+    pub net_restrict: String,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub fn execute(self) {
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-03-28T17:04:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
+
+    /// NAT port mapping mechanism (e.g., "any", "none", "upnp", "pmp", "pmp:<IP>", "extip:<IP>").
+    #[arg(long, default_value = "none")]
+    pub nat: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-03-28T17:04:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// Private key as hex (for testing purposes).
+    #[arg(long, default_value = "")]
+    pub node_key_hex: String,
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-03-28T17:04:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+use clap::Parser;
+
+/// The arguments for the `reth bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Write out the node's public key and quit.
+    #[arg(long)]
+    pub write_address: bool,
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-03-28T17:05:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: cool, we should log a smol info! log that the service is running at this address
created_at: 2025-04-02T22:45:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: you don't really need to wrap this into an async scope and can use .await directly
created_at: 2025-04-02T22:46:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
+
+            let discv4_updates = discv4_service.update_stream();
+            let discv4_service = discv4_service.spawn();
+
+            Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: we're now dropping all of this on L71

but we likely want to use this to emit some infos occasionally, so what we can do here is some 

`while let Som(ev) = discv4_updates.next().await {}` loop and logging some info
created_at: 2025-04-02T22:49:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
+
+            let discv4_updates = discv4_service.update_stream();
+            let discv4_service = discv4_service.spawn();
+
+            Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
+        };
+
+        let discv5_future = async {
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: this also doesn't need a new async scope
created_at: 2025-04-02T22:49:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
+
+            let discv4_updates = discv4_service.update_stream();
+            let discv4_service = discv4_service.spawn();
+
+            Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
+        };
+
+        let discv5_future = async {
+            if self.v5 == false {
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: I think we could invert this instead
```suggestion
            if self.v5 == false {
```
created_at: 2025-04-02T22:51:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
+
+            let discv4_updates = discv4_service.update_stream();
+            let discv4_service = discv4_service.spawn();
+
+            Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
+        };
+
+        let discv5_future = async {
+            if self.v5 == false {
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-04-06T16:52:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
+            let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+            let (discv4, mut discv4_service) =
+                Discv4::bind(socket_addr, local_enr, sk, config).await.map_err(|err| {
+                    NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr))
+                })?;
+
+            let discv4_updates = discv4_service.update_stream();
+            let discv4_service = discv4_service.spawn();
+
+            Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
+        };
+
+        let discv5_future = async {
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-04-06T16:52:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        println!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let discv4_future = async {
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-04-06T16:52:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+use tokio_stream::StreamExt;
+use tracing::info;
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        info!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+        let (_discv4, mut discv4_service) = Discv4::bind(socket_addr, local_enr, sk, config)
+            .await
+            .map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr)))?;
+
+        info!("Started discv4 at address:{:?}", socket_addr);
+
+        let mut discv4_updates = discv4_service.update_stream();
+        discv4_service.spawn();
+
+        if self.v5 {
+            info!("Starting discv5");
+            let config = Config::builder(socket_addr).build();
+            let (_discv5, _discv5_updates, _local_enr_discv5) =
+                Discv5::start(&sk, config).await.map_err(NetworkError::Discv5Error)?;
+        };
+
+        while let Some(update) = discv4_updates.next().await {
+            match update {
+                DiscoveryUpdate::Added(record) => {
+                    info!("new peer added, peer_id={:?}", record.id);
+                }
+                DiscoveryUpdate::Removed(peer_id) => {
+                    info!("peer with peer-id={:?} removed", peer_id);
+                }
+                _ => {}
+            }
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: we want the same here for discv5 updates

so ideally we can combine this with a tokio::select over both even variants
created_at: 2025-04-07T16:32:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+use clap::Parser;
+use rand::thread_rng;
+use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
+use reth_discv5::{Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use secp256k1::SECP256K1;
+use std::{net::SocketAddr, str::FromStr};
+use tokio_stream::StreamExt;
+use tracing::info;
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        info!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+        let mut rng = thread_rng();
+        let (sk, _pk) = SECP256K1.generate_keypair(&mut rng);
+
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+        let (_discv4, mut discv4_service) = Discv4::bind(socket_addr, local_enr, sk, config)
+            .await
+            .map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr)))?;
+
+        info!("Started discv4 at address:{:?}", socket_addr);
+
+        let mut discv4_updates = discv4_service.update_stream();
+        discv4_service.spawn();
+
+        if self.v5 {
+            info!("Starting discv5");
+            let config = Config::builder(socket_addr).build();
+            let (_discv5, _discv5_updates, _local_enr_discv5) =
+                Discv5::start(&sk, config).await.map_err(NetworkError::Discv5Error)?;
+        };
+
+        while let Some(update) = discv4_updates.next().await {
+            match update {
+                DiscoveryUpdate::Added(record) => {
+                    info!("new peer added, peer_id={:?}", record.id);
+                }
+                DiscoveryUpdate::Removed(peer_id) => {
+                    info!("peer with peer-id={:?} removed", peer_id);
+                }
+                _ => {}
+            }
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-04-07T18:42:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +53,8 @@ pub enum Subcommands {
     Version,
     /// Returns the full database path
     Path,
+    /// Runs a bootnode for the discovery protocol
+    Bootnode(bootnode::Command),
path: crates/cli/commands/src/db/mod.rs
user: login: mattsse
body: this doesn't quite fit under db commands, can make this a standalone command instead and then enable for the reth/op cli struct instead:

https://github.com/paradigmxyz/reth/blob/main/bin/reth/src/cli/mod.rs#L211-L211


created_at: 2025-04-15T13:33:47Z
author_association: COLLABORATOR
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: let's move this to the p2p folder instead
created_at: 2025-04-15T13:34:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,114 @@
+use clap::Parser;
+use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
+use reth_discv5::{discv5::Event, Config, Discv5};
+use reth_net_nat::NatResolver;
+use reth_network::error::{NetworkError, ServiceKind};
+use reth_network_peers::NodeRecord;
+use std::{net::SocketAddr, str::FromStr};
+use tokio::select;
+use tokio_stream::StreamExt;
+use tracing::info;
+
+/// The arguments for the `reth db bootnode` command.
+/// see https://github.com/ethereum/go-ethereum/blob/14eb8967be7acc54c5dc9a416151ac45c01251b6/cmd/bootnode/main.go#L39-L48
+/// for ref
+#[derive(Parser, Debug)]
+pub struct Command {
+    /// Listen address for the bootnode (default: ":30301").
+    #[arg(long, default_value = ":30301")]
+    pub addr: String,
+
+    /// Generate a new node key and save it to the specified file.
+    #[arg(long, default_value = "")]
+    pub gen_key: String,
+
+    /// Private key filename for the node.
+    #[arg(long, default_value = "")]
+    pub node_key: String,
+
+    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
+    #[arg(long, default_value = "any")]
+    pub nat: NatResolver,
+
+    /// Run a v5 topic discovery bootnode.
+    #[arg(long)]
+    pub v5: bool,
+}
+
+impl Command {
+    pub async fn execute(self) -> Result<(), NetworkError> {
+        info!("Bootnode started with config: {:?}", self);
+        // generate a (random) keypair
+
+        let sk = reth_network::config::rng_secret_key();
+        let socket_addr = SocketAddr::from_str(&self.addr).expect("Invalid addr");
+        let local_enr = NodeRecord::from_secret_key(socket_addr, &sk);
+
+        let config = Discv4Config::builder().external_ip_resolver(Some(self.nat)).build();
+
+        let (_discv4, mut discv4_service) = Discv4::bind(socket_addr, local_enr, sk, config)
+            .await
+            .map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(socket_addr)))?;
+
+        info!("Started discv4 at address:{:?}", socket_addr);
+
+        let mut discv4_updates = discv4_service.update_stream();
+        discv4_service.spawn();
+
+        // Optional discv5 update event listener if v5 is enabled
+        let mut discv5_updates = None;
+
+        if self.v5 {
+            info!("Starting discv5");
+            let config = Config::builder(socket_addr).build();
+            let (_discv5, updates, _local_enr_discv5) =
+                Discv5::start(&sk, config).await.map_err(NetworkError::Discv5Error)?;
+            discv5_updates = Some(updates);
+        };
+
+        // event info loop for logging
+        loop {
+            select! {
+                //discv4 updates
+                update = discv4_updates.next() => {
+                    if let Some(update) = update {
+                        match update {
+                            DiscoveryUpdate::Added(record) => {
+                                info!("(Discv4) new peer added, peer_id={:?}", record.id);
+                            }
+                            DiscoveryUpdate::Removed(peer_id) => {
+                                info!("(Discv4) peer with peer-id={:?} removed", peer_id);
+                            }
+                            _ => {}
+                        }
+                    } else {
+                        info!("(Discv4) update stream ended.");
+                        break;
+                    }
+                }
+                //if discv5, discv5 update stream, else do nothing
+                update = async {
+                    if let Some(updates) = &mut discv5_updates {
+                        updates.recv().await
+                    } else {
+                        futures::future::pending().await
+                    }
+                } => {
path: crates/cli/commands/src/db/bootnode.rs
user: login: mattsse
body: TIL
created_at: 2025-04-15T13:34:14Z
author_association: COLLABORATOR
path: crates/cli/commands/src/db/bootnode.rs
user: login: frankudoags
body: done
created_at: 2025-04-17T07:58:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: frankudoags
created_at: 2025-03-28T15:44:35Z
body: PTAL @mattsse also a bit confused here on how to run the discovery stack

author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-03-28T16:42:04Z
body: Nice I see. Thank you. Main blocker was trying to figure out how I’d use the Discovery service as is. 

But this works fine. I can proceed to build the Discv4 and v5 manually. Will also remove the commands we do not need. Thank you @mattsse 
author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-04-01T14:28:04Z
body: some smol delay apologies, PTAL @mattsse, made some progress I hope

some issues I'm facing:
- I currently am generating a new secret key using rand, but `ThreadRng` from rand is not Send, causing an error here:
https://github.com/paradigmxyz/reth/blob/8f3a316dbd6d25f6cce7e01a635509beafd0fc3f/bin/reth/src/cli/mod.rs#L192-L194

because:
https://github.com/paradigmxyz/reth/blob/8f3a316dbd6d25f6cce7e01a635509beafd0fc3f/crates/cli/runner/src/lib.rs#L111-L115

-  Again, just curious, if gen key is passed, I generate the new private key and use it, but if node_key is passed, I read the content of the file at that path, assume that is a secret key and use it, right?
author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-04-01T14:30:30Z
body: Also wdyt about:
1. The use of Network error as the error type
2.  the current impl of discv4 and discv5?
 @mattsse 
author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-04-07T16:37:34Z
body: > cool, only have one more request for observability
> 
> the rand issue we'll figure out

I kept wondering what I was doing wrong when you said it compiles fine for you. Thanks for clearing that up. 


Wanted to add some file stuff for the gen key and node key or do I ignore that?

Also wanted to add new doc file to book.


author_association: CONTRIBUTOR
user: login: mattsse
created_at: 2025-04-07T16:43:01Z
body: the book will be updated by running `make update-book-cli`

> Wanted to add some file stuff for the gen key and node key or do I ignore that?

we can skip that for now
author_association: COLLABORATOR
user: login: frankudoags
created_at: 2025-04-07T16:47:26Z
body: > the book will be updated by running `make update-book-cli`
>  
> > Wanted to add some file stuff for the gen key and node key or do I ignore that?
> 
> we can skip that for now

Alright, Will work on the discv5 updates and book update right away. This has been on for too long. 😭😂
author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-04-07T18:45:17Z
body: @mattsse event logs done, book update fails as build fails


todo left:
- gen key and node key impl (low priority)
- fix rand not Send issue(curious as to how we'd resolve this btw, kindly tag me to soln if possible)
- run make update-book-cli
author_association: CONTRIBUTOR
user: login: frankudoags
created_at: 2025-04-18T12:49:51Z
body: > ty! fixes the last warnings by exporting the type.
> 
> next step would be adding this to the cli structs

noted. Appreciate all the help and clean ups. Would address next steps in a new pr
author_association: CONTRIBUTOR