diff --git a/crates/ethereum/node/src/payload.rs b/crates/ethereum/node/src/payload.rs index 644078bc96..4189ee327b 100644 --- a/crates/ethereum/node/src/payload.rs +++ b/crates/ethereum/node/src/payload.rs @@ -55,7 +55,7 @@ where EthereumBuilderConfig::new() .with_gas_limit(gas_limit) .with_max_blobs_per_block(conf.max_blobs_per_block()) - .with_extra_data(conf.extra_data_bytes()), + .with_extra_data(conf.extra_data()), )) } } diff --git a/crates/node/core/src/args/payload_builder.rs b/crates/node/core/src/args/payload_builder.rs index d9ce66499f..d63452d4f5 100644 --- a/crates/node/core/src/args/payload_builder.rs +++ b/crates/node/core/src/args/payload_builder.rs @@ -1,5 +1,6 @@ use crate::{cli::config::PayloadBuilderConfig, version::default_extra_data}; use alloy_consensus::constants::MAXIMUM_EXTRA_DATA_SIZE; +use alloy_primitives::Bytes; use clap::{ builder::{RangedU64ValueParser, TypedValueParser}, Arg, Args, Command, @@ -8,7 +9,7 @@ use reth_cli_util::{ parse_duration_from_secs, parse_duration_from_secs_or_ms, parsers::format_duration_as_secs_or_ms, }; -use std::{borrow::Cow, ffi::OsStr, sync::OnceLock, time::Duration}; +use std::{ffi::OsStr, sync::OnceLock, time::Duration}; /// Global static payload builder defaults static PAYLOAD_BUILDER_DEFAULTS: OnceLock = OnceLock::new(); @@ -80,12 +81,15 @@ impl Default for DefaultPayloadBuilderValues { #[command(next_help_heading = "Builder")] pub struct PayloadBuilderArgs { /// Block extra data set by the payload builder. + /// + /// If the value is a `0x`-prefixed hex string, it is decoded into raw bytes. Otherwise, the + /// raw UTF-8 bytes of the string are used. #[arg( long = "builder.extradata", value_parser = ExtraDataValueParser::default(), - default_value_t = DefaultPayloadBuilderValues::get_global().extra_data.clone() + default_value = DefaultPayloadBuilderValues::get_global().extra_data.as_str() )] - pub extra_data: String, + pub extra_data: Bytes, /// Target gas limit for built blocks. #[arg(long = "builder.gaslimit", alias = "miner.gaslimit", value_name = "GAS_LIMIT")] @@ -130,7 +134,7 @@ impl Default for PayloadBuilderArgs { fn default() -> Self { let defaults = DefaultPayloadBuilderValues::get_global(); Self { - extra_data: defaults.extra_data.clone(), + extra_data: Bytes::from(defaults.extra_data.as_bytes().to_vec()), interval: parse_duration_from_secs_or_ms(defaults.interval.as_str()).unwrap(), gas_limit: None, deadline: Duration::from_secs(defaults.deadline.parse().unwrap()), @@ -141,8 +145,8 @@ impl Default for PayloadBuilderArgs { } impl PayloadBuilderConfig for PayloadBuilderArgs { - fn extra_data(&self) -> Cow<'_, str> { - self.extra_data.as_str().into() + fn extra_data(&self) -> Bytes { + self.extra_data.clone() } fn interval(&self) -> Duration { @@ -171,7 +175,7 @@ impl PayloadBuilderConfig for PayloadBuilderArgs { struct ExtraDataValueParser; impl TypedValueParser for ExtraDataValueParser { - type Value = String; + type Value = Bytes; fn parse_ref( &self, @@ -181,7 +185,19 @@ impl TypedValueParser for ExtraDataValueParser { ) -> Result { let val = value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?; - if val.len() > MAXIMUM_EXTRA_DATA_SIZE { + + let bytes = if let Some(hex) = val.strip_prefix("0x") { + alloy_primitives::hex::decode(hex).map_err(|e| { + clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + format!("Invalid hex in extradata: {e}"), + ) + })? + } else { + val.as_bytes().to_vec() + }; + + if bytes.len() > MAXIMUM_EXTRA_DATA_SIZE { return Err(clap::Error::raw( clap::error::ErrorKind::InvalidValue, format!( @@ -189,7 +205,8 @@ impl TypedValueParser for ExtraDataValueParser { ), )) } - Ok(val.to_string()) + + Ok(bytes.into()) } } @@ -232,7 +249,7 @@ mod tests { extra_data.as_str(), ]) .args; - assert_eq!(args.extra_data, extra_data); + assert_eq!(args.extra_data.as_ref(), extra_data.as_bytes()); } #[test] @@ -246,6 +263,49 @@ mod tests { assert!(args.is_err()); } + #[test] + fn test_valid_hex_extra_data() { + let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE)); + let args = CommandParser::::parse_from([ + "reth", + "--builder.extradata", + hex.as_str(), + ]) + .args; + assert_eq!(args.extra_data.as_ref(), vec![0xab; MAXIMUM_EXTRA_DATA_SIZE].as_slice()); + } + + #[test] + fn test_oversized_hex_extra_data() { + let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1)); + assert!(CommandParser::::try_parse_from([ + "reth", + "--builder.extradata", + hex.as_str(), + ]) + .is_err()); + } + + #[test] + fn test_invalid_hex_extra_data() { + assert!(CommandParser::::try_parse_from([ + "reth", + "--builder.extradata", + "0xZZZZ", + ]) + .is_err()); + } + + #[test] + fn test_odd_length_hex_extra_data() { + assert!(CommandParser::::try_parse_from([ + "reth", + "--builder.extradata", + "0xabc", + ]) + .is_err()); + } + #[test] fn payload_builder_args_default_sanity_check() { let default_args = PayloadBuilderArgs::default(); diff --git a/crates/node/core/src/cli/config.rs b/crates/node/core/src/cli/config.rs index 4d5a485b59..48b9ea0001 100644 --- a/crates/node/core/src/cli/config.rs +++ b/crates/node/core/src/cli/config.rs @@ -5,7 +5,7 @@ use alloy_primitives::Bytes; use reth_chainspec::{Chain, ChainKind, NamedChain}; use reth_network::{protocol::IntoRlpxSubProtocol, NetworkPrimitives}; use reth_transaction_pool::PoolConfig; -use std::{borrow::Cow, time::Duration}; +use std::time::Duration; /// 60M gas limit const ETHEREUM_BLOCK_GAS_LIMIT_60M: u64 = 60_000_000; @@ -15,13 +15,8 @@ const ETHEREUM_BLOCK_GAS_LIMIT_60M: u64 = 60_000_000; /// This provides all basic payload builder settings and is implemented by the /// [`PayloadBuilderArgs`](crate::args::PayloadBuilderArgs) type. pub trait PayloadBuilderConfig { - /// Block extra data set by the payload builder. - fn extra_data(&self) -> Cow<'_, str>; - /// Returns the extra data as bytes. - fn extra_data_bytes(&self) -> Bytes { - self.extra_data().as_bytes().to_vec().into() - } + fn extra_data(&self) -> Bytes; /// The interval at which the job should build a new payload after the last. fn interval(&self) -> Duration; diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index 80f2aebdc4..a962a0b111 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -685,7 +685,9 @@ TxPool: Builder: --builder.extradata - Block extra data set by the payload builder + Block extra data set by the payload builder. + + If the value is a `0x`-prefixed hex string, it is decoded into raw bytes. Otherwise, the raw UTF-8 bytes of the string are used. [default: reth//] diff --git a/examples/custom-engine-types/src/main.rs b/examples/custom-engine-types/src/main.rs index e572e208e4..6e7e52b39d 100644 --- a/examples/custom-engine-types/src/main.rs +++ b/examples/custom-engine-types/src/main.rs @@ -339,7 +339,7 @@ where pool, evm_config, EthereumBuilderConfig::new() - .with_extra_data(ctx.payload_builder_config().extra_data_bytes()), + .with_extra_data(ctx.payload_builder_config().extra_data()), ), }; Ok(payload_builder) diff --git a/examples/custom-payload-builder/src/main.rs b/examples/custom-payload-builder/src/main.rs index 33eb92483f..0d20e82199 100644 --- a/examples/custom-payload-builder/src/main.rs +++ b/examples/custom-payload-builder/src/main.rs @@ -62,8 +62,7 @@ where ctx.provider().clone(), pool, evm_config, - EthereumBuilderConfig::new() - .with_extra_data(ctx.payload_builder_config().extra_data_bytes()), + EthereumBuilderConfig::new().with_extra_data(ctx.payload_builder_config().extra_data()), ); let conf = ctx.payload_builder_config();