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