Skip to main content

reth_node_core/args/
payload_builder.rs

1use crate::{cli::config::PayloadBuilderConfig, version::default_extra_data};
2use alloy_consensus::constants::MAXIMUM_EXTRA_DATA_SIZE;
3use alloy_primitives::Bytes;
4use clap::{
5    builder::{RangedU64ValueParser, TypedValueParser},
6    Arg, Args, Command,
7};
8use reth_cli_util::{
9    parse_duration_from_secs, parse_duration_from_secs_or_ms,
10    parsers::format_duration_as_secs_or_ms,
11};
12use std::{ffi::OsStr, sync::OnceLock, time::Duration};
13
14/// Global static payload builder defaults
15static PAYLOAD_BUILDER_DEFAULTS: OnceLock<DefaultPayloadBuilderValues> = OnceLock::new();
16
17/// Default values for payload builder that can be customized
18///
19/// Global defaults can be set via [`DefaultPayloadBuilderValues::try_init`].
20#[derive(Debug, Clone)]
21pub struct DefaultPayloadBuilderValues {
22    /// Default extra data for blocks
23    extra_data: String,
24    /// Default interval between payload builds in seconds
25    interval: String,
26    /// Default deadline for payload builds in seconds
27    deadline: String,
28    /// Default maximum number of concurrent payload building tasks
29    max_payload_tasks: usize,
30}
31
32impl DefaultPayloadBuilderValues {
33    /// Initialize the global payload builder defaults with this configuration
34    pub fn try_init(self) -> Result<(), Self> {
35        PAYLOAD_BUILDER_DEFAULTS.set(self)
36    }
37
38    /// Get a reference to the global payload builder defaults
39    pub fn get_global() -> &'static Self {
40        PAYLOAD_BUILDER_DEFAULTS.get_or_init(Self::default)
41    }
42
43    /// Set the default extra data
44    pub fn with_extra_data(mut self, v: impl Into<String>) -> Self {
45        self.extra_data = v.into();
46        self
47    }
48
49    /// Set the default interval in seconds
50    pub fn with_interval(mut self, v: Duration) -> Self {
51        self.interval = format_duration_as_secs_or_ms(v);
52        self
53    }
54
55    /// Set the default deadline in seconds
56    pub fn with_deadline(mut self, v: u64) -> Self {
57        self.deadline = format!("{}", v);
58        self
59    }
60
61    /// Set the default maximum payload tasks
62    pub const fn with_max_payload_tasks(mut self, v: usize) -> Self {
63        self.max_payload_tasks = v;
64        self
65    }
66}
67
68impl Default for DefaultPayloadBuilderValues {
69    fn default() -> Self {
70        Self {
71            extra_data: default_extra_data(),
72            interval: "1".to_string(),
73            deadline: "12".to_string(),
74            max_payload_tasks: 3,
75        }
76    }
77}
78
79/// Parameters for configuring the Payload Builder
80#[derive(Debug, Clone, Args, PartialEq, Eq)]
81#[command(next_help_heading = "Builder")]
82pub struct PayloadBuilderArgs {
83    /// Block extra data set by the payload builder.
84    ///
85    /// If the value is a `0x`-prefixed hex string, it is decoded into raw bytes. Otherwise, the
86    /// raw UTF-8 bytes of the string are used.
87    #[arg(
88        long = "builder.extradata",
89        value_parser = ExtraDataValueParser::default(),
90        default_value = DefaultPayloadBuilderValues::get_global().extra_data.as_str()
91    )]
92    pub extra_data: Bytes,
93
94    /// Target gas limit for built blocks.
95    #[arg(long = "builder.gaslimit", alias = "miner.gaslimit", value_name = "GAS_LIMIT")]
96    pub gas_limit: Option<u64>,
97
98    /// The interval at which the job should build a new payload after the last.
99    ///
100    /// Interval is specified in seconds or in milliseconds if the value ends with `ms`:
101    ///   * `50ms` -> 50 milliseconds
102    ///   * `1` -> 1 second
103    #[arg(
104        long = "builder.interval",
105        value_parser = parse_duration_from_secs_or_ms,
106        default_value = DefaultPayloadBuilderValues::get_global().interval.as_str(),
107        value_name = "DURATION"
108    )]
109    pub interval: Duration,
110
111    /// The deadline for when the payload builder job should resolve.
112    #[arg(
113        long = "builder.deadline",
114        value_parser = parse_duration_from_secs,
115        default_value = DefaultPayloadBuilderValues::get_global().deadline.as_str(),
116        value_name = "SECONDS"
117    )]
118    pub deadline: Duration,
119
120    /// Maximum number of tasks to spawn for building a payload.
121    #[arg(
122        long = "builder.max-tasks",
123        value_parser = RangedU64ValueParser::<usize>::new().range(1..),
124        default_value_t = DefaultPayloadBuilderValues::get_global().max_payload_tasks
125    )]
126    pub max_payload_tasks: usize,
127
128    /// Maximum number of blobs to include per block.
129    #[arg(long = "builder.max-blobs", value_name = "COUNT")]
130    pub max_blobs_per_block: Option<u64>,
131}
132
133impl Default for PayloadBuilderArgs {
134    fn default() -> Self {
135        let defaults = DefaultPayloadBuilderValues::get_global();
136        Self {
137            extra_data: Bytes::from(defaults.extra_data.as_bytes().to_vec()),
138            interval: parse_duration_from_secs_or_ms(defaults.interval.as_str()).unwrap(),
139            gas_limit: None,
140            deadline: Duration::from_secs(defaults.deadline.parse().unwrap()),
141            max_payload_tasks: defaults.max_payload_tasks,
142            max_blobs_per_block: None,
143        }
144    }
145}
146
147impl PayloadBuilderConfig for PayloadBuilderArgs {
148    fn extra_data(&self) -> Bytes {
149        self.extra_data.clone()
150    }
151
152    fn interval(&self) -> Duration {
153        self.interval
154    }
155
156    fn deadline(&self) -> Duration {
157        self.deadline
158    }
159
160    fn gas_limit(&self) -> Option<u64> {
161        self.gas_limit
162    }
163
164    fn max_payload_tasks(&self) -> usize {
165        self.max_payload_tasks
166    }
167
168    fn max_blobs_per_block(&self) -> Option<u64> {
169        self.max_blobs_per_block
170    }
171}
172
173#[derive(Clone, Debug, Default)]
174#[non_exhaustive]
175struct ExtraDataValueParser;
176
177impl TypedValueParser for ExtraDataValueParser {
178    type Value = Bytes;
179
180    fn parse_ref(
181        &self,
182        _cmd: &Command,
183        _arg: Option<&Arg>,
184        value: &OsStr,
185    ) -> Result<Self::Value, clap::Error> {
186        let val =
187            value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
188
189        let bytes = if let Some(hex) = val.strip_prefix("0x") {
190            alloy_primitives::hex::decode(hex).map_err(|e| {
191                clap::Error::raw(
192                    clap::error::ErrorKind::InvalidValue,
193                    format!("Invalid hex in extradata: {e}"),
194                )
195            })?
196        } else {
197            val.as_bytes().to_vec()
198        };
199
200        if bytes.len() > MAXIMUM_EXTRA_DATA_SIZE {
201            return Err(clap::Error::raw(
202                clap::error::ErrorKind::InvalidValue,
203                format!(
204                    "Payload builder extradata size exceeds {MAXIMUM_EXTRA_DATA_SIZE}-byte limit"
205                ),
206            ))
207        }
208
209        Ok(bytes.into())
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use clap::Parser;
217
218    /// A helper type to parse Args more easily
219    #[derive(Parser)]
220    struct CommandParser<T: Args> {
221        #[command(flatten)]
222        args: T,
223    }
224
225    #[test]
226    fn test_args_with_valid_max_tasks() {
227        let args =
228            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.max-tasks", "1"])
229                .args;
230        assert_eq!(args.max_payload_tasks, 1)
231    }
232
233    #[test]
234    fn test_args_with_invalid_max_tasks() {
235        assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
236            "reth",
237            "--builder.max-tasks",
238            "0"
239        ])
240        .is_err());
241    }
242
243    #[test]
244    fn test_default_extra_data() {
245        let extra_data = default_extra_data();
246        let args = CommandParser::<PayloadBuilderArgs>::parse_from([
247            "reth",
248            "--builder.extradata",
249            extra_data.as_str(),
250        ])
251        .args;
252        assert_eq!(args.extra_data.as_ref(), extra_data.as_bytes());
253    }
254
255    #[test]
256    fn test_invalid_extra_data() {
257        let extra_data = "x".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1);
258        let args = CommandParser::<PayloadBuilderArgs>::try_parse_from([
259            "reth",
260            "--builder.extradata",
261            extra_data.as_str(),
262        ]);
263        assert!(args.is_err());
264    }
265
266    #[test]
267    fn test_valid_hex_extra_data() {
268        let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE));
269        let args = CommandParser::<PayloadBuilderArgs>::parse_from([
270            "reth",
271            "--builder.extradata",
272            hex.as_str(),
273        ])
274        .args;
275        assert_eq!(args.extra_data.as_ref(), vec![0xab; MAXIMUM_EXTRA_DATA_SIZE].as_slice());
276    }
277
278    #[test]
279    fn test_oversized_hex_extra_data() {
280        let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1));
281        assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
282            "reth",
283            "--builder.extradata",
284            hex.as_str(),
285        ])
286        .is_err());
287    }
288
289    #[test]
290    fn test_invalid_hex_extra_data() {
291        assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
292            "reth",
293            "--builder.extradata",
294            "0xZZZZ",
295        ])
296        .is_err());
297    }
298
299    #[test]
300    fn test_odd_length_hex_extra_data() {
301        assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
302            "reth",
303            "--builder.extradata",
304            "0xabc",
305        ])
306        .is_err());
307    }
308
309    #[test]
310    fn payload_builder_args_default_sanity_check() {
311        let default_args = PayloadBuilderArgs::default();
312        let args = CommandParser::<PayloadBuilderArgs>::parse_from(["reth"]).args;
313        assert_eq!(args, default_args);
314    }
315
316    #[test]
317    fn test_args_with_s_interval() {
318        let args =
319            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50"])
320                .args;
321        assert_eq!(args.interval, Duration::from_secs(50));
322    }
323
324    #[test]
325    fn test_args_with_ms_interval() {
326        let args =
327            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50ms"])
328                .args;
329        assert_eq!(args.interval, Duration::from_millis(50));
330    }
331}