diff --git a/Cargo.lock b/Cargo.lock index be3197e487..a154188bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9058,7 +9058,6 @@ dependencies = [ "reth-basic-payload-builder", "reth-chain-state", "reth-chainspec", - "reth-cli-util", "reth-config", "reth-consensus", "reth-consensus-debug-client", diff --git a/crates/cli/commands/src/p2p/mod.rs b/crates/cli/commands/src/p2p/mod.rs index c72ceca78e..d255f5fd77 100644 --- a/crates/cli/commands/src/p2p/mod.rs +++ b/crates/cli/commands/src/p2p/mod.rs @@ -8,7 +8,7 @@ use backon::{ConstantBuilder, Retryable}; use clap::{Parser, Subcommand}; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; use reth_cli::chainspec::ChainSpecParser; -use reth_cli_util::{get_secret_key, hash_or_num_value_parser}; +use reth_cli_util::hash_or_num_value_parser; use reth_config::Config; use reth_network::{BlockDownloaderProvider, NetworkConfigBuilder}; use reth_network_p2p::bodies::client::BodiesClient; @@ -183,9 +183,7 @@ impl DownloadArgs { config.peers.trusted_nodes_only = self.network.trusted_only; let default_secret_key_path = data_dir.p2p_secret(); - let secret_key_path = - self.network.p2p_secret_key.clone().unwrap_or(default_secret_key_path); - let p2p_secret_key = get_secret_key(&secret_key_path)?; + let p2p_secret_key = self.network.secret_key(default_secret_key_path)?; let rlpx_socket = (self.network.addr, self.network.port).into(); let boot_nodes = self.chain.bootnodes().unwrap_or_default(); diff --git a/crates/cli/util/src/lib.rs b/crates/cli/util/src/lib.rs index 7e0d69c186..5fe648f303 100644 --- a/crates/cli/util/src/lib.rs +++ b/crates/cli/util/src/lib.rs @@ -12,7 +12,7 @@ pub mod allocator; /// Helper function to load a secret key from a file. pub mod load_secret_key; -pub use load_secret_key::get_secret_key; +pub use load_secret_key::{get_secret_key, parse_secret_key_from_hex}; /// Cli parsers functions. pub mod parsers; diff --git a/crates/cli/util/src/load_secret_key.rs b/crates/cli/util/src/load_secret_key.rs index 0ca46398f1..64d756cddc 100644 --- a/crates/cli/util/src/load_secret_key.rs +++ b/crates/cli/util/src/load_secret_key.rs @@ -30,6 +30,10 @@ pub enum SecretKeyError { /// Path to the secret key file. secret_file: PathBuf, }, + + /// Invalid hex string format. + #[error("invalid hex string: {0}")] + InvalidHexString(String), } /// Attempts to load a [`SecretKey`] from a specified path. If no file exists there, then it @@ -60,3 +64,75 @@ pub fn get_secret_key(secret_key_path: &Path) -> Result Result { + // Remove "0x" prefix if present + let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); + + // Decode the hex string + let bytes = alloy_primitives::hex::decode(hex_str) + .map_err(|e| SecretKeyError::InvalidHexString(e.to_string()))?; + + // Parse into SecretKey + SecretKey::from_slice(&bytes).map_err(SecretKeyError::SecretKeyDecodeError) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_secret_key_from_hex_without_prefix() { + // Valid 32-byte hex string (64 characters) + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_ok()); + + let secret_key = result.unwrap(); + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex); + } + + #[test] + fn test_parse_secret_key_from_hex_with_0x_prefix() { + // Valid 32-byte hex string with 0x prefix + let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_ok()); + + let secret_key = result.unwrap(); + let expected = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), expected); + } + + #[test] + fn test_parse_secret_key_from_hex_invalid_length() { + // Invalid length (not 32 bytes) + let hex = "4c0883a69102937d"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + } + + #[test] + fn test_parse_secret_key_from_hex_invalid_chars() { + // Invalid hex characters + let hex = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + + if let Err(SecretKeyError::InvalidHexString(_)) = result { + // Expected error type + } else { + panic!("Expected InvalidHexString error"); + } + } + + #[test] + fn test_parse_secret_key_from_hex_empty() { + let hex = ""; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + } +} diff --git a/crates/node/builder/Cargo.toml b/crates/node/builder/Cargo.toml index 8e8774e86c..df89dcfdf5 100644 --- a/crates/node/builder/Cargo.toml +++ b/crates/node/builder/Cargo.toml @@ -15,7 +15,6 @@ workspace = true ## reth reth-chain-state.workspace = true reth-chainspec.workspace = true -reth-cli-util.workspace = true reth-config.workspace = true reth-consensus-debug-client.workspace = true reth-consensus.workspace = true diff --git a/crates/node/builder/src/builder/mod.rs b/crates/node/builder/src/builder/mod.rs index f2886f4756..b905b60a0d 100644 --- a/crates/node/builder/src/builder/mod.rs +++ b/crates/node/builder/src/builder/mod.rs @@ -12,7 +12,6 @@ use crate::{ use alloy_eips::eip4844::env_settings::EnvKzgSettings; use futures::Future; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; -use reth_cli_util::get_secret_key; use reth_db_api::{database::Database, database_metrics::DatabaseMetrics}; use reth_exex::ExExContext; use reth_network::{ @@ -869,9 +868,7 @@ impl BuilderContext { /// Get the network secret from the given data dir fn network_secret(&self, data_dir: &ChainPath) -> eyre::Result { - let network_secret_path = - self.config().network.p2p_secret_key.clone().unwrap_or_else(|| data_dir.p2p_secret()); - let secret_key = get_secret_key(&network_secret_path)?; + let secret_key = self.config().network.secret_key(data_dir.p2p_secret())?; Ok(secret_key) } diff --git a/crates/node/core/src/args/network.rs b/crates/node/core/src/args/network.rs index 9d6598cca1..3b7bfef87b 100644 --- a/crates/node/core/src/args/network.rs +++ b/crates/node/core/src/args/network.rs @@ -10,6 +10,7 @@ use std::{ use crate::version::version_metadata; use clap::Args; use reth_chainspec::EthChainSpec; +use reth_cli_util::{get_secret_key, load_secret_key::SecretKeyError}; use reth_config::Config; use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT}; use reth_discv5::{ @@ -81,9 +82,16 @@ pub struct NetworkArgs { /// /// This will also deterministically set the peer ID. If not specified, it will be set in the /// data dir for the chain being used. - #[arg(long, value_name = "PATH")] + #[arg(long, value_name = "PATH", conflicts_with = "p2p_secret_key_hex")] pub p2p_secret_key: Option, + /// Hex encoded secret key to use for this node. + /// + /// This will also deterministically set the peer ID. Cannot be used together with + /// `--p2p-secret-key`. + #[arg(long, value_name = "HEX", conflicts_with = "p2p_secret_key")] + pub p2p_secret_key_hex: Option, + /// Do not persist peers. #[arg(long, verbatim_doc_comment)] pub no_persist_peers: bool, @@ -351,6 +359,25 @@ impl NetworkArgs { ) .await } + + /// Load the p2p secret key from the provided options. + /// + /// If `p2p_secret_key_hex` is provided, it will be used directly. + /// If `p2p_secret_key` is provided, it will be loaded from the file. + /// If neither is provided, the `default_secret_key_path` will be used. + pub fn secret_key( + &self, + default_secret_key_path: PathBuf, + ) -> Result { + if let Some(b256) = &self.p2p_secret_key_hex { + // Use the B256 value directly (already validated as 32 bytes) + SecretKey::from_slice(b256.as_slice()).map_err(SecretKeyError::SecretKeyDecodeError) + } else { + // Load from file (either provided path or default) + let secret_key_path = self.p2p_secret_key.clone().unwrap_or(default_secret_key_path); + get_secret_key(&secret_key_path) + } + } } impl Default for NetworkArgs { @@ -364,6 +391,7 @@ impl Default for NetworkArgs { peers_file: None, identity: version_metadata().p2p_client_version.to_string(), p2p_secret_key: None, + p2p_secret_key_hex: None, no_persist_peers: false, nat: NatResolver::Any, addr: DEFAULT_DISCOVERY_ADDR, @@ -698,4 +726,53 @@ mod tests { let args = CommandParser::::parse_from(["reth"]).args; assert!(args.required_block_hashes.is_empty()); } + + #[test] + fn parse_p2p_secret_key_hex() { + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let expected: B256 = hex.parse().unwrap(); + assert_eq!(args.p2p_secret_key_hex, Some(expected)); + assert_eq!(args.p2p_secret_key, None); + } + + #[test] + fn parse_p2p_secret_key_hex_with_0x_prefix() { + let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let expected: B256 = hex.parse().unwrap(); + assert_eq!(args.p2p_secret_key_hex, Some(expected)); + assert_eq!(args.p2p_secret_key, None); + } + + #[test] + fn test_p2p_secret_key_and_hex_are_mutually_exclusive() { + let result = CommandParser::::try_parse_from([ + "reth", + "--p2p-secret-key", + "/path/to/key", + "--p2p-secret-key-hex", + "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f", + ]); + + assert!(result.is_err()); + } + + #[test] + fn test_secret_key_method_with_hex() { + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let temp_dir = std::env::temp_dir(); + let default_path = temp_dir.join("default_key"); + let secret_key = args.secret_key(default_path).unwrap(); + + // Verify the secret key matches the hex input + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex); + } } diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index bdf39a671f..c0bd56cdfa 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -160,6 +160,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. diff --git a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx index e9c8ff08cd..67b32df5f3 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx @@ -106,6 +106,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. diff --git a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx index 0dc48503a8..8fe415be39 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx @@ -106,6 +106,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. diff --git a/docs/vocs/docs/pages/cli/reth/stage/run.mdx b/docs/vocs/docs/pages/cli/reth/stage/run.mdx index c56bede6d0..a0314b73ba 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/run.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/run.mdx @@ -210,6 +210,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers.