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_eips::{eip1559::ETHEREUM_BLOCK_GAS_LIMIT_36M, merge::SLOT_DURATION};
4use clap::{
5    builder::{RangedU64ValueParser, TypedValueParser},
6    Arg, Args, Command,
7};
8use reth_cli_util::{parse_duration_from_secs, parse_duration_from_secs_or_ms};
9use std::{borrow::Cow, ffi::OsStr, time::Duration};
10
11/// Parameters for configuring the Payload Builder
12#[derive(Debug, Clone, Args, PartialEq, Eq)]
13#[command(next_help_heading = "Builder")]
14pub struct PayloadBuilderArgs {
15    /// Block extra data set by the payload builder.
16    #[arg(long = "builder.extradata", value_parser = ExtraDataValueParser::default(), default_value_t = default_extra_data())]
17    pub extra_data: String,
18
19    /// Target gas limit for built blocks.
20    #[arg(long = "builder.gaslimit", default_value_t = ETHEREUM_BLOCK_GAS_LIMIT_36M, value_name = "GAS_LIMIT")]
21    pub gas_limit: u64,
22
23    /// The interval at which the job should build a new payload after the last.
24    ///
25    /// Interval is specified in seconds or in milliseconds if the value ends with `ms`:
26    ///   * `50ms` -> 50 milliseconds
27    ///   * `1` -> 1 second
28    #[arg(long = "builder.interval", value_parser = parse_duration_from_secs_or_ms, default_value = "1", value_name = "DURATION")]
29    pub interval: Duration,
30
31    /// The deadline for when the payload builder job should resolve.
32    #[arg(long = "builder.deadline", value_parser = parse_duration_from_secs, default_value = "12", value_name = "SECONDS")]
33    pub deadline: Duration,
34
35    /// Maximum number of tasks to spawn for building a payload.
36    #[arg(long = "builder.max-tasks", default_value = "3", value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
37    pub max_payload_tasks: usize,
38}
39
40impl Default for PayloadBuilderArgs {
41    fn default() -> Self {
42        Self {
43            extra_data: default_extra_data(),
44            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_36M,
45            interval: Duration::from_secs(1),
46            deadline: SLOT_DURATION,
47            max_payload_tasks: 3,
48        }
49    }
50}
51
52impl PayloadBuilderConfig for PayloadBuilderArgs {
53    fn extra_data(&self) -> Cow<'_, str> {
54        self.extra_data.as_str().into()
55    }
56
57    fn interval(&self) -> Duration {
58        self.interval
59    }
60
61    fn deadline(&self) -> Duration {
62        self.deadline
63    }
64
65    fn gas_limit(&self) -> u64 {
66        self.gas_limit
67    }
68
69    fn max_payload_tasks(&self) -> usize {
70        self.max_payload_tasks
71    }
72}
73
74#[derive(Clone, Debug, Default)]
75#[non_exhaustive]
76struct ExtraDataValueParser;
77
78impl TypedValueParser for ExtraDataValueParser {
79    type Value = String;
80
81    fn parse_ref(
82        &self,
83        _cmd: &Command,
84        _arg: Option<&Arg>,
85        value: &OsStr,
86    ) -> Result<Self::Value, clap::Error> {
87        let val =
88            value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
89        if val.len() > MAXIMUM_EXTRA_DATA_SIZE {
90            return Err(clap::Error::raw(
91                clap::error::ErrorKind::InvalidValue,
92                format!(
93                    "Payload builder extradata size exceeds {MAXIMUM_EXTRA_DATA_SIZE}-byte limit"
94                ),
95            ))
96        }
97        Ok(val.to_string())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use clap::Parser;
105
106    /// A helper type to parse Args more easily
107    #[derive(Parser)]
108    struct CommandParser<T: Args> {
109        #[command(flatten)]
110        args: T,
111    }
112
113    #[test]
114    fn test_args_with_valid_max_tasks() {
115        let args =
116            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.max-tasks", "1"])
117                .args;
118        assert_eq!(args.max_payload_tasks, 1)
119    }
120
121    #[test]
122    fn test_args_with_invalid_max_tasks() {
123        assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
124            "reth",
125            "--builder.max-tasks",
126            "0"
127        ])
128        .is_err());
129    }
130
131    #[test]
132    fn test_default_extra_data() {
133        let extra_data = default_extra_data();
134        let args = CommandParser::<PayloadBuilderArgs>::parse_from([
135            "reth",
136            "--builder.extradata",
137            extra_data.as_str(),
138        ])
139        .args;
140        assert_eq!(args.extra_data, extra_data);
141    }
142
143    #[test]
144    fn test_invalid_extra_data() {
145        let extra_data = "x".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1);
146        let args = CommandParser::<PayloadBuilderArgs>::try_parse_from([
147            "reth",
148            "--builder.extradata",
149            extra_data.as_str(),
150        ]);
151        assert!(args.is_err());
152    }
153
154    #[test]
155    fn payload_builder_args_default_sanity_check() {
156        let default_args = PayloadBuilderArgs::default();
157        let args = CommandParser::<PayloadBuilderArgs>::parse_from(["reth"]).args;
158        assert_eq!(args, default_args);
159    }
160
161    #[test]
162    fn test_args_with_s_interval() {
163        let args =
164            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50"])
165                .args;
166        assert_eq!(args.interval, Duration::from_secs(50));
167    }
168
169    #[test]
170    fn test_args_with_ms_interval() {
171        let args =
172            CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50ms"])
173                .args;
174        assert_eq!(args.interval, Duration::from_millis(50));
175    }
176}